polyrob 0.5.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.
- agents/README.md +453 -0
- agents/__init__.py +164 -0
- agents/base_agent.py +743 -0
- agents/personality/character.py +119 -0
- agents/personality/character_manager.py +309 -0
- agents/personality/characters/rob.character.json +56 -0
- agents/personality/characters/trump.character.json +63 -0
- agents/personality/persona_render.py +78 -0
- agents/personality/persona_resolver.py +115 -0
- agents/prompt/__init__.py +58 -0
- agents/prompt/base_prompt.py +92 -0
- agents/prompt/system.py +417 -0
- agents/task/README_PATH_MANAGEMENT.md +141 -0
- agents/task/__init__.py +162 -0
- agents/task/agent/__init__.py +70 -0
- agents/task/agent/agent_state.py +408 -0
- agents/task/agent/async_delegation.py +274 -0
- agents/task/agent/autonomy_state.py +275 -0
- agents/task/agent/conversation.py +75 -0
- agents/task/agent/core/__init__.py +6 -0
- agents/task/agent/core/aux_metering.py +61 -0
- agents/task/agent/core/background_review.py +157 -0
- agents/task/agent/core/construction.py +1179 -0
- agents/task/agent/core/conversational_exit.py +49 -0
- agents/task/agent/core/correspondent_gate.py +227 -0
- agents/task/agent/core/curator.py +246 -0
- agents/task/agent/core/episodic_digest.py +103 -0
- agents/task/agent/core/error_recovery.py +480 -0
- agents/task/agent/core/history_io.py +257 -0
- agents/task/agent/core/llm_provisioning.py +287 -0
- agents/task/agent/core/llm_runner.py +547 -0
- agents/task/agent/core/logging_io.py +276 -0
- agents/task/agent/core/loop_detection.py +262 -0
- agents/task/agent/core/memory_prefetch.py +325 -0
- agents/task/agent/core/memory_writer.py +667 -0
- agents/task/agent/core/model_introspection.py +82 -0
- agents/task/agent/core/model_swap.py +177 -0
- agents/task/agent/core/next_action_internal.py +1331 -0
- agents/task/agent/core/output_validation.py +303 -0
- agents/task/agent/core/project_context.py +272 -0
- agents/task/agent/core/resources.py +93 -0
- agents/task/agent/core/result_processing.py +389 -0
- agents/task/agent/core/run_loop.py +661 -0
- agents/task/agent/core/safety_lifecycle.py +228 -0
- agents/task/agent/core/secret_guard.py +293 -0
- agents/task/agent/core/self_wake.py +280 -0
- agents/task/agent/core/session_metadata.py +104 -0
- agents/task/agent/core/step.py +781 -0
- agents/task/agent/core/step_execution.py +398 -0
- agents/task/agent/core/step_telemetry.py +222 -0
- agents/task/agent/core/test_aux_metering.py +50 -0
- agents/task/agent/core/test_billing_failover.py +68 -0
- agents/task/agent/core/test_fatal_error_classifier.py +38 -0
- agents/task/agent/core/test_judge_validation.py +275 -0
- agents/task/agent/core/test_model_swap.py +190 -0
- agents/task/agent/core/test_reflection_offload.py +54 -0
- agents/task/agent/core/turn_input.py +8 -0
- agents/task/agent/core/untrusted_wrap.py +103 -0
- agents/task/agent/core/user_ingress.py +228 -0
- agents/task/agent/hitl_manager.py +396 -0
- agents/task/agent/log_sanitize.py +36 -0
- agents/task/agent/message_manager/config.py +97 -0
- agents/task/agent/message_manager/service.py +758 -0
- agents/task/agent/message_manager/tests.py +305 -0
- agents/task/agent/message_manager/tool_call_builder.py +539 -0
- agents/task/agent/message_manager/tool_message_repair.py +364 -0
- agents/task/agent/message_manager/views.py +143 -0
- agents/task/agent/messages/__init__.py +8 -0
- agents/task/agent/messages/builders.py +309 -0
- agents/task/agent/messages/compactor.py +611 -0
- agents/task/agent/messages/context_references.py +271 -0
- agents/task/agent/messages/filters.py +333 -0
- agents/task/agent/messages/guidance.py +328 -0
- agents/task/agent/messages/persistence.py +448 -0
- agents/task/agent/messages/retrieval.py +506 -0
- agents/task/agent/messages/sqlite_persistence.py +85 -0
- agents/task/agent/messages/test_compaction_upgrade.py +349 -0
- agents/task/agent/messages/test_persistence_subdir_fix.py +28 -0
- agents/task/agent/messages/test_sqlite_persistence.py +54 -0
- agents/task/agent/messages/token_counter.py +532 -0
- agents/task/agent/orchestrator.py +776 -0
- agents/task/agent/profile_manager.py +105 -0
- agents/task/agent/profile_registry.py +173 -0
- agents/task/agent/prompts.py +1090 -0
- agents/task/agent/scenario_registry.py +226 -0
- agents/task/agent/service.py +509 -0
- agents/task/agent/session.py +984 -0
- agents/task/agent/skill_discovery.py +110 -0
- agents/task/agent/skill_frontmatter.py +94 -0
- agents/task/agent/skill_manager.py +1201 -0
- agents/task/agent/skill_store.py +627 -0
- agents/task/agent/skill_validation.py +56 -0
- agents/task/agent/skill_writer.py +492 -0
- agents/task/agent/sub_agent_manager.py +1137 -0
- agents/task/agent/test_session_disk_recovery.py +34 -0
- agents/task/agent/tests.py +513 -0
- agents/task/agent/tool_call_tracker.py +511 -0
- agents/task/agent/views.py +469 -0
- agents/task/config.py +432 -0
- agents/task/constants.py +1384 -0
- agents/task/flag_defaults.py +63 -0
- agents/task/goals/__init__.py +1 -0
- agents/task/goals/autonomy_marker.py +28 -0
- agents/task/goals/board.py +690 -0
- agents/task/goals/completion_judge.py +247 -0
- agents/task/goals/context.py +62 -0
- agents/task/goals/dispatcher.py +848 -0
- agents/task/goals/escalation.py +102 -0
- agents/task/goals/planner.py +114 -0
- agents/task/logging_config.py +155 -0
- agents/task/path.py +991 -0
- agents/task/robust_parse_config.py +400 -0
- agents/task/runtime/__init__.py +0 -0
- agents/task/runtime/run_as_session.py +130 -0
- agents/task/runtime_safety.py +308 -0
- agents/task/session/__init__.py +5 -0
- agents/task/session/browser_pool.py +130 -0
- agents/task/session/cleanup.py +612 -0
- agents/task/session/execution.py +397 -0
- agents/task/session/feed.py +200 -0
- agents/task/session/hitl_ingress.py +201 -0
- agents/task/session/hooks.py +83 -0
- agents/task/session/multi_agent.py +243 -0
- agents/task/session/workspace.py +91 -0
- agents/task/session_registry.py +91 -0
- agents/task/session_route.py +35 -0
- agents/task/sqlite_session_registry.py +261 -0
- agents/task/surface_config.py +185 -0
- agents/task/telemetry/__init__.py +58 -0
- agents/task/telemetry/event_log.py +180 -0
- agents/task/telemetry/formatters.py +642 -0
- agents/task/telemetry/manager.py +571 -0
- agents/task/telemetry/memory_events.py +66 -0
- agents/task/telemetry/self_events.py +53 -0
- agents/task/telemetry/sequence.py +160 -0
- agents/task/telemetry/service.py +1265 -0
- agents/task/telemetry/views.py +882 -0
- agents/task/templates.py +98 -0
- agents/task/test_aux_chain.py +151 -0
- agents/task/test_aux_router.py +41 -0
- agents/task/tests/test_parse_robustness.py +170 -0
- agents/task/tool_defaults.py +110 -0
- agents/task/utils.py +762 -0
- agents/task/utils_json.py +768 -0
- agents/task/utils_webview.py +86 -0
- agents/task/workspace_context.py +229 -0
- agents/task_agent_lite.py +2398 -0
- api/README.md +683 -0
- api/TASK_API_DOCS.md +151 -0
- api/__init__.py +0 -0
- api/a2a/__init__.py +37 -0
- api/a2a/agent_card.py +337 -0
- api/a2a/client.py +657 -0
- api/a2a/endpoints.py +375 -0
- api/a2a/models.py +268 -0
- api/a2a/streaming.py +419 -0
- api/a2a/task_handler.py +929 -0
- api/admin_endpoints.py +1584 -0
- api/app.py +923 -0
- api/auth_constants.py +28 -0
- api/auth_endpoints.py +402 -0
- api/auth_state.py +36 -0
- api/chat_via_task.py +42 -0
- api/conversation_manager.py +231 -0
- api/dependencies.py +186 -0
- api/eip8004_endpoints.py +452 -0
- api/hyperliquid_models.py +174 -0
- api/hyperliquid_routes.py +450 -0
- api/interfaces.py +207 -0
- api/jwt_middleware.py +83 -0
- api/kb/__init__.py +1 -0
- api/kb/endpoints.py +314 -0
- api/mcp_models.py +279 -0
- api/mcp_routes.py +438 -0
- api/middleware.py +526 -0
- api/models.py +176 -0
- api/openai_compat/__init__.py +0 -0
- api/openai_compat/model_map.py +25 -0
- api/openai_compat/models.py +59 -0
- api/openai_compat/router.py +140 -0
- api/payment_endpoints.py +444 -0
- api/payment_verification.py +138 -0
- api/polymarket_models.py +155 -0
- api/polymarket_routes.py +501 -0
- api/pricing_endpoints.py +89 -0
- api/session_routing.py +54 -0
- api/skill_endpoints.py +453 -0
- api/task_http_api.py +1856 -0
- api/webhooks.py +52 -0
- api/x402_endpoints.py +280 -0
- avatar/README.md +44 -0
- avatar/__init__.py +1 -0
- avatar/config/rob.json +7 -0
- avatar/mindprint.js +395 -0
- avatar/renders/rob.meta.json +20 -0
- avatar/renders/rob.png +0 -0
- avatar/studio.html +373 -0
- avatar/webview/avatar-live.js +44 -0
- cli/README.md +89 -0
- cli/__init__.py +0 -0
- cli/cli_surface.py +67 -0
- cli/commands/__init__.py +0 -0
- cli/commands/_bootstrap.py +49 -0
- cli/commands/_errors.py +53 -0
- cli/commands/chat.py +973 -0
- cli/commands/config.py +100 -0
- cli/commands/dashboard.py +103 -0
- cli/commands/doctor.py +222 -0
- cli/commands/email.py +174 -0
- cli/commands/gateway.py +346 -0
- cli/commands/goals.py +509 -0
- cli/commands/init.py +276 -0
- cli/commands/journey.py +30 -0
- cli/commands/kb.py +280 -0
- cli/commands/model.py +107 -0
- cli/commands/owner.py +431 -0
- cli/commands/pfp.py +232 -0
- cli/commands/run.py +414 -0
- cli/commands/serve.py +41 -0
- cli/commands/session.py +684 -0
- cli/commands/skill_install.py +786 -0
- cli/commands/skills.py +128 -0
- cli/commands/subagents.py +100 -0
- cli/commands/surface.py +81 -0
- cli/commands/telegram.py +185 -0
- cli/commands/todos.py +209 -0
- cli/commands/tools.py +102 -0
- cli/commands/update.py +316 -0
- cli/commands/whatsapp.py +175 -0
- cli/config_store.py +276 -0
- cli/gitignore.py +43 -0
- cli/inventory.py +46 -0
- cli/keys.py +101 -0
- cli/persona.py +17 -0
- cli/polyrob.py +137 -0
- cli/ui/__init__.py +64 -0
- cli/ui/activity.py +141 -0
- cli/ui/app.py +538 -0
- cli/ui/banner.py +233 -0
- cli/ui/blocks.py +534 -0
- cli/ui/bootstrap_notice.py +47 -0
- cli/ui/commands/__init__.py +41 -0
- cli/ui/commands/h_cron.py +64 -0
- cli/ui/commands/h_journey.py +164 -0
- cli/ui/commands/h_kb.py +121 -0
- cli/ui/commands/h_learn.py +100 -0
- cli/ui/commands/h_mcp.py +215 -0
- cli/ui/commands/h_self.py +113 -0
- cli/ui/commands/h_skills.py +314 -0
- cli/ui/commands/handlers.py +1672 -0
- cli/ui/commands/registry.py +426 -0
- cli/ui/dialog.py +425 -0
- cli/ui/event_registry.py +110 -0
- cli/ui/event_specs.py +45 -0
- cli/ui/events.py +433 -0
- cli/ui/identity.py +15 -0
- cli/ui/lifecycle.py +159 -0
- cli/ui/live_hooks.py +43 -0
- cli/ui/model_selector.py +532 -0
- cli/ui/persistent_loop.py +181 -0
- cli/ui/pick.py +113 -0
- cli/ui/plain_renderer.py +351 -0
- cli/ui/renderer.py +384 -0
- cli/ui/rich_renderer.py +533 -0
- cli/ui/secrets.py +115 -0
- cli/ui/state.py +376 -0
- cli/ui/statusbar.py +231 -0
- cli/ui/streaming.py +254 -0
- cli/ui/terminal_render.py +105 -0
- cli/ui/theme.py +132 -0
- cli/update/__init__.py +6 -0
- cli/update/context.py +121 -0
- cli/update/detect.py +176 -0
- cli/update/engine.py +95 -0
- cli/update/migrate_guarded.py +55 -0
- cli/update/process_guard.py +215 -0
- cli/update/runners.py +78 -0
- cli/update/snapshot.py +270 -0
- cli/update/versions.py +205 -0
- core/README.md +377 -0
- core/__init__.py +174 -0
- core/assets.py +87 -0
- core/async_bridge.py +76 -0
- core/autonomy_runtime.py +259 -0
- core/base_component.py +302 -0
- core/bootstrap.py +681 -0
- core/bot.py +310 -0
- core/config.py +833 -0
- core/constants.py +126 -0
- core/container.py +462 -0
- core/db_manifest.py +101 -0
- core/embedding.py +57 -0
- core/env.py +39 -0
- core/exceptions.py +287 -0
- core/flags.py +144 -0
- core/flags_catalog.py +345 -0
- core/home_migration.py +62 -0
- core/identity.py +118 -0
- core/initialization.py +1004 -0
- core/instance.py +537 -0
- core/interactive_gate.py +96 -0
- core/logging.py +436 -0
- core/owner_doc_writer.py +176 -0
- core/pairing.py +193 -0
- core/path_safety.py +20 -0
- core/paths.py +27 -0
- core/payment_config.py +21 -0
- core/permissions.py +322 -0
- core/runtime_config.py +96 -0
- core/runtime_paths.py +135 -0
- core/seams.py +35 -0
- core/secret_scan.py +25 -0
- core/secret_scrub.py +71 -0
- core/secrets.py +35 -0
- core/security_logging_filter.py +148 -0
- core/self_context_writer.py +333 -0
- core/self_evolution.py +294 -0
- core/session_context.py +47 -0
- core/sqlite_util.py +59 -0
- core/surfaces/__init__.py +16 -0
- core/surfaces/access.py +118 -0
- core/surfaces/binding.py +88 -0
- core/surfaces/bootstrap.py +78 -0
- core/surfaces/circuit.py +168 -0
- core/surfaces/continuity.py +77 -0
- core/surfaces/correspondents.py +228 -0
- core/surfaces/dispatcher.py +216 -0
- core/surfaces/envelopes.py +75 -0
- core/surfaces/gc.py +30 -0
- core/surfaces/idempotency.py +57 -0
- core/surfaces/inbound_webhook.py +124 -0
- core/surfaces/media.py +32 -0
- core/surfaces/message_router.py +91 -0
- core/surfaces/outbound_allowlist.py +80 -0
- core/surfaces/outbound_dispatcher.py +110 -0
- core/surfaces/outbound_mirror.py +37 -0
- core/surfaces/outbound_queue.py +107 -0
- core/surfaces/outbound_target.py +12 -0
- core/surfaces/owner_admin.py +29 -0
- core/surfaces/proactive.py +78 -0
- core/surfaces/progress.py +108 -0
- core/surfaces/rate_bucket.py +21 -0
- core/surfaces/registry.py +51 -0
- core/surfaces/rendering.py +53 -0
- core/surfaces/send_policy.py +14 -0
- core/surfaces/serialize.py +24 -0
- core/surfaces/session_chat_registry.py +112 -0
- core/surfaces/session_policy.py +60 -0
- core/surfaces/surface.py +288 -0
- core/surfaces/transcription.py +88 -0
- core/surfaces/voice_echo.py +26 -0
- core/surfaces/voice_guard.py +23 -0
- core/tickers.py +174 -0
- core/tool_catalog.py +192 -0
- core/version.py +75 -0
- core/wallet/__init__.py +5 -0
- core/wallet/agent_wallet.py +59 -0
- core/wallet/audit_sink.py +61 -0
- core/wallet/config.py +70 -0
- core/wallet/factory.py +87 -0
- core/wallet/policy.py +104 -0
- core/wallet/signer.py +51 -0
- cron/README.md +65 -0
- cron/__init__.py +0 -0
- cron/delivery.py +277 -0
- cron/digest.py +124 -0
- cron/jobs.py +207 -0
- cron/runner.py +297 -0
- cron/schedule.py +194 -0
- cron/scheduler.py +201 -0
- cron/service.py +57 -0
- cron/wake_gate.py +223 -0
- data/__init__.py +1 -0
- data/prompts/autov2_prompts.json +87 -0
- data/prompts/skills/browser-automation/SKILL.md +43 -0
- data/prompts/skills/coding-workflow/SKILL.md +36 -0
- data/prompts/skills/crypto-trading-safety/SKILL.md +53 -0
- data/prompts/skills/document-writing/SKILL.md +33 -0
- data/prompts/skills/email-comms/SKILL.md +33 -0
- data/prompts/skills/file-data-ops/SKILL.md +35 -0
- data/prompts/skills/hyperliquid-account-review/SKILL.md +48 -0
- data/prompts/skills/hyperliquid-market-data/SKILL.md +52 -0
- data/prompts/skills/hyperliquid-trading/SKILL.md +59 -0
- data/prompts/skills/lead-research/SKILL.md +86 -0
- data/prompts/skills/market-research-brief/SKILL.md +104 -0
- data/prompts/skills/person-analyzer/SKILL.md +124 -0
- data/prompts/skills/polymarket-market-research/SKILL.md +54 -0
- data/prompts/skills/polymarket-portfolio-review/SKILL.md +50 -0
- data/prompts/skills/polymarket-trading/SKILL.md +55 -0
- data/prompts/skills/presentation-creator/SKILL.md +36 -0
- data/prompts/skills/project-analyzer/SKILL.md +169 -0
- data/prompts/skills/rules.json +268 -0
- data/prompts/skills/secret-handling/SKILL.md +20 -0
- data/prompts/skills/skill-authoring/SKILL.md +31 -0
- data/prompts/skills/skill-security-review/SKILL.md +27 -0
- data/prompts/skills/social-discovery/SKILL.md +57 -0
- data/prompts/skills/task-planning/SKILL.md +33 -0
- data/prompts/skills/web-research/SKILL.md +39 -0
- data/prompts/skills/web-scraping/SKILL.md +73 -0
- data/prompts/skills/x-engagement/SKILL.md +69 -0
- data/prompts/system_prompts.json +4 -0
- data/subscription_channels.json +5 -0
- modules/README.md +503 -0
- modules/__init__.py +124 -0
- modules/auth/__init__.py +13 -0
- modules/auth/api_key_manager.py +157 -0
- modules/auth/identity_mapper.py +380 -0
- modules/auth/siwe_auth.py +273 -0
- modules/auth/tier_manager.py +173 -0
- modules/base_module.py +190 -0
- modules/credits/__init__.py +29 -0
- modules/credits/balance_manager.py +237 -0
- modules/credits/cost_utils.py +129 -0
- modules/credits/pricing.py +139 -0
- modules/credits/unified_ledger.py +148 -0
- modules/credits/usage_meter.py +310 -0
- modules/credits/usage_tracker.py +674 -0
- modules/database/__init__.py +33 -0
- modules/database/audit_log.py +322 -0
- modules/database/auth_tables.py +276 -0
- modules/database/connection.py +644 -0
- modules/database/connection_pool.py +305 -0
- modules/database/conversation_contexts.py +446 -0
- modules/database/database_manager.py +237 -0
- modules/database/hyperliquid.py +375 -0
- modules/database/polymarket.py +574 -0
- modules/database/user_mcp_servers.py +728 -0
- modules/database/user_profiles.py +562 -0
- modules/database/utils.py +107 -0
- modules/database/x402_tables.py +92 -0
- modules/eip8004/README.md +1069 -0
- modules/eip8004/__init__.py +65 -0
- modules/eip8004/contracts.py +498 -0
- modules/eip8004/models.py +271 -0
- modules/eip8004/registration.py +148 -0
- modules/eip8004/reputation.py +367 -0
- modules/eip8004/validation.py +277 -0
- modules/llm/__init__.py +97 -0
- modules/llm/adapters.py +963 -0
- modules/llm/anthropic_client.py +831 -0
- modules/llm/available_models.py +106 -0
- modules/llm/brain_scrubber.py +137 -0
- modules/llm/cache_hints.py +162 -0
- modules/llm/deepseek_client.py +562 -0
- modules/llm/gemini_client.py +1367 -0
- modules/llm/llm_client.py +1139 -0
- modules/llm/llm_client_registry.py +188 -0
- modules/llm/llm_factory.py +237 -0
- modules/llm/llm_manager.py +1036 -0
- modules/llm/messages.py +374 -0
- modules/llm/model_registry.py +1935 -0
- modules/llm/nvidia_client.py +125 -0
- modules/llm/openai_client.py +800 -0
- modules/llm/openrouter_client.py +969 -0
- modules/llm/profiles.py +178 -0
- modules/llm/test_anthropic_cache_capture.py +35 -0
- modules/llm/test_cached_pricing_derivation.py +24 -0
- modules/llm/think_scrubber.py +444 -0
- modules/llm/token_counter.py +268 -0
- modules/memory/__init__.py +35 -0
- modules/memory/backend_factory.py +83 -0
- modules/memory/cache_manager.py +186 -0
- modules/memory/episodic.py +108 -0
- modules/memory/local_vector_memory_provider.py +552 -0
- modules/memory/memory_manager.py +225 -0
- modules/memory/models.py +312 -0
- modules/memory/provider.py +210 -0
- modules/memory/registry.py +205 -0
- modules/memory/sqlite_memory_provider.py +767 -0
- modules/memory/task/__init__.py +52 -0
- modules/memory/task/compaction_manager.py +157 -0
- modules/memory/task/context_retriever.py +809 -0
- modules/memory/task/hierarchical_memory.py +1002 -0
- modules/memory/task/lexical_retriever.py +71 -0
- modules/memory/task/null_context_manager.py +213 -0
- modules/memory/task/phase_manager.py +625 -0
- modules/memory/task/reflection_service.py +157 -0
- modules/memory/task/semantic_retriever.py +218 -0
- modules/memory/task/task_context_manager.py +1165 -0
- modules/memory/task/test_forgetting_engages.py +24 -0
- modules/memory/task/test_reflection_llm.py +39 -0
- modules/memory/task/test_threat_scan.py +53 -0
- modules/memory/task/threat_scan.py +90 -0
- modules/memory/test_sqlite_memory_provider.py +106 -0
- modules/memory/user_profile_manager.py +527 -0
- modules/payments/__init__.py +11 -0
- modules/payments/deposit_monitor.py +478 -0
- modules/payments/price_oracle.py +70 -0
- modules/payments/treasury_sweeper.py +401 -0
- modules/payments/wallet_generator.py +145 -0
- modules/pfp/__init__.py +1 -0
- modules/pfp/config.py +92 -0
- modules/pfp/mesh.py +565 -0
- modules/pfp/push.py +69 -0
- modules/pfp/renderer.py +85 -0
- modules/pfp/store.py +82 -0
- modules/skills/__init__.py +1 -0
- modules/skills/skill_usage.py +248 -0
- modules/transcription/__init__.py +24 -0
- modules/transcription/base.py +20 -0
- modules/transcription/faster_whisper_transcriber.py +55 -0
- modules/x402/README.md +249 -0
- modules/x402/__init__.py +22 -0
- modules/x402/invoicing.py +416 -0
- modules/x402/middleware.py +364 -0
- modules/x402/settlement_watcher.py +145 -0
- modules/x402/x402_integration.py +344 -0
- polyrob-0.5.0.dist-info/METADATA +482 -0
- polyrob-0.5.0.dist-info/RECORD +745 -0
- polyrob-0.5.0.dist-info/WHEEL +5 -0
- polyrob-0.5.0.dist-info/entry_points.txt +2 -0
- polyrob-0.5.0.dist-info/licenses/LICENSE +21 -0
- polyrob-0.5.0.dist-info/top_level.txt +12 -0
- surfaces/README.md +70 -0
- surfaces/__init__.py +0 -0
- surfaces/email/__init__.py +0 -0
- surfaces/email/dedup.py +44 -0
- surfaces/email/harness.py +187 -0
- surfaces/email/inbound.py +159 -0
- surfaces/email/seed.py +60 -0
- surfaces/email/surface.py +105 -0
- surfaces/telegram/__init__.py +0 -0
- surfaces/telegram/dedup.py +83 -0
- surfaces/telegram/harness.py +886 -0
- surfaces/telegram/inbound.py +145 -0
- surfaces/telegram/interactive_tools.py +49 -0
- surfaces/telegram/markdown.py +347 -0
- surfaces/telegram/rate_limit.py +167 -0
- surfaces/telegram/surface.py +187 -0
- surfaces/telegram/voice.py +52 -0
- surfaces/whatsapp/__init__.py +0 -0
- surfaces/whatsapp/client.py +51 -0
- surfaces/whatsapp/harness.py +62 -0
- surfaces/whatsapp/inbound.py +132 -0
- surfaces/whatsapp/surface.py +64 -0
- surfaces/whatsapp/window.py +34 -0
- tools/README.md +578 -0
- tools/__init__.py +424 -0
- tools/alchemy/__init__.py +5 -0
- tools/alchemy/alchemy_tool.py +389 -0
- tools/anysite/__init__.py +17 -0
- tools/anysite/client.py +88 -0
- tools/anysite/tool.py +89 -0
- tools/base_tool.py +468 -0
- tools/browser/__init__.py +22 -0
- tools/browser/actions.py +118 -0
- tools/browser/browser.py +1457 -0
- tools/browser/browser_manager.py +690 -0
- tools/browser/context.py +1748 -0
- tools/browser/playwright_utils.py +137 -0
- tools/browser/views.py +56 -0
- tools/code_exec/SANDBOX_SECURITY.md +144 -0
- tools/code_exec/__init__.py +178 -0
- tools/code_exec/backend.py +73 -0
- tools/code_exec/backends/__init__.py +1 -0
- tools/code_exec/backends/docker.py +779 -0
- tools/code_exec/backends/local_subprocess.py +154 -0
- tools/code_exec/env_policy.py +45 -0
- tools/code_exec/result.py +42 -0
- tools/code_exec/sandbox_guard.py +59 -0
- tools/code_exec/tool.py +227 -0
- tools/coding/__init__.py +50 -0
- tools/coding/edit.py +107 -0
- tools/coding/search.py +113 -0
- tools/coding/tool.py +353 -0
- tools/collabland/__init__.py +5 -0
- tools/collabland/collabland_tool.py +471 -0
- tools/controller/__init__.py +0 -0
- tools/controller/_helpers.py +165 -0
- tools/controller/action_registration.py +1765 -0
- tools/controller/approval.py +244 -0
- tools/controller/approval_interactive.py +90 -0
- tools/controller/delegation.py +195 -0
- tools/controller/execution.py +797 -0
- tools/controller/execution_context.py +141 -0
- tools/controller/hooks.py +135 -0
- tools/controller/introspection.py +344 -0
- tools/controller/mcp_registrar.py +190 -0
- tools/controller/message_send.py +31 -0
- tools/controller/registry/__init__.py +4 -0
- tools/controller/registry/schema_generators.py +395 -0
- tools/controller/registry/schema_sanitizer.py +406 -0
- tools/controller/registry/service.py +1266 -0
- tools/controller/registry/views.py +151 -0
- tools/controller/service.py +409 -0
- tools/controller/tool_management.py +334 -0
- tools/controller/types.py +28 -0
- tools/controller/views.py +450 -0
- tools/cronjob_tools.py +166 -0
- tools/crypto_trade_gate.py +59 -0
- tools/descriptors.py +452 -0
- tools/dom/__init__.py +0 -0
- tools/dom/history_tree_processor/service.py +244 -0
- tools/dom/history_tree_processor/view.py +99 -0
- tools/dom/service.py +471 -0
- tools/dom/views.py +346 -0
- tools/email_tool.py +403 -0
- tools/exceptions.py +169 -0
- tools/filesystem.py +1038 -0
- tools/filesystem_docproc.py +331 -0
- tools/filesystem_pdf.py +755 -0
- tools/git/__init__.py +42 -0
- tools/git/tool.py +258 -0
- tools/github/__init__.py +33 -0
- tools/github/client.py +94 -0
- tools/github/tool.py +237 -0
- tools/goal_tools.py +309 -0
- tools/hyperliquid/__init__.py +56 -0
- tools/hyperliquid/models.py +233 -0
- tools/hyperliquid/service.py +1680 -0
- tools/knowledge_ingest.py +950 -0
- tools/mcp/README.md +535 -0
- tools/mcp/__init__.py +52 -0
- tools/mcp/catalog.py +115 -0
- tools/mcp/config.py +337 -0
- tools/mcp/mcp_tool.py +1415 -0
- tools/mcp/param_coercion.py +289 -0
- tools/mcp/protocol.py +1520 -0
- tools/mcp/rate_limit.py +50 -0
- tools/mcp/security.py +424 -0
- tools/mcp/self_install.py +111 -0
- tools/mcp/server_manager.py +829 -0
- tools/mcp/subscriptions.py +72 -0
- tools/mcp/user_mcp_service.py +899 -0
- tools/mcp/validation_tracker.py +89 -0
- tools/mcp/views.py +346 -0
- tools/oauth/__init__.py +17 -0
- tools/oauth/manager.py +75 -0
- tools/oauth/provider.py +69 -0
- tools/oauth/providers/__init__.py +1 -0
- tools/oauth/providers/generic_oauth2.py +96 -0
- tools/perplexity_tool.py +280 -0
- tools/polymarket/__init__.py +41 -0
- tools/polymarket/clob_adapter.py +76 -0
- tools/polymarket/models.py +182 -0
- tools/polymarket/service.py +2082 -0
- tools/self_env/__init__.py +61 -0
- tools/self_env/tool.py +274 -0
- tools/shell/__init__.py +80 -0
- tools/shell/backend_pool.py +118 -0
- tools/shell/discipline.py +62 -0
- tools/shell/executor.py +157 -0
- tools/shell/loopback_allow.py +87 -0
- tools/shell/process_registry.py +113 -0
- tools/shell/process_tool.py +131 -0
- tools/shell/state.py +129 -0
- tools/shell/tool.py +151 -0
- tools/task_tool.py +637 -0
- tools/twitter_tool.py +2086 -0
- tools/user_directory.py +179 -0
- tools/web_fetch/__init__.py +3 -0
- tools/web_fetch/fetcher.py +144 -0
- tools/web_fetch/render.py +44 -0
- tools/web_fetch/tool.py +60 -0
- tools/x402/__init__.py +60 -0
- tools/x402/client.py +47 -0
- tools/x402/invoice_tool.py +143 -0
- tools/x402/real_client.py +250 -0
- tools/x402/service.py +131 -0
- utils/README.md +498 -0
- utils/__init__.py +46 -0
- utils/auth_utils.py +114 -0
- utils/bounded_collections.py +162 -0
- utils/circuit_breaker.py +389 -0
- utils/gif_utils.py +435 -0
- utils/markdown_utils.py +291 -0
- utils/message_utils.py +147 -0
- utils/metrics.py +36 -0
- utils/path_validator.py +227 -0
- utils/rate_limit_manager.py +399 -0
- utils/result_size.py +296 -0
- utils/time_utils.py +206 -0
- utils/user_utils.py +188 -0
- webview/README.md +111 -0
- webview/RENAME_AND_ALIGN_HANDOFF.md +200 -0
- webview/__init__.py +8 -0
- webview/activity.py +636 -0
- webview/owner_auth.py +108 -0
- webview/pages.py +448 -0
- webview/repair_sessions.py +399 -0
- webview/server.py +4323 -0
- webview/server_launcher.py +112 -0
- webview/static/css/activity.css +89 -0
- webview/static/css/chat.css +1772 -0
- webview/static/css/components.css +1190 -0
- webview/static/css/config-panel.css +651 -0
- webview/static/css/pages/admin.css +766 -0
- webview/static/css/pages/profile.css +231 -0
- webview/static/css/pages/settings.css +297 -0
- webview/static/css/pages/signin.css +316 -0
- webview/static/css/style.css +4250 -0
- webview/static/css/variables.css +147 -0
- webview/static/css/workspace-fullwidth-fix.css +109 -0
- webview/static/img/favicon.ico +0 -0
- webview/static/js/activity.js +394 -0
- webview/static/js/admin/activity.js +186 -0
- webview/static/js/admin/dashboard.js +163 -0
- webview/static/js/admin/user_detail.js +400 -0
- webview/static/js/admin/users.js +174 -0
- webview/static/js/admin/utils.js +214 -0
- webview/static/js/chat.js +3178 -0
- webview/static/js/config-panel.js +522 -0
- webview/static/js/constants.js +20 -0
- webview/static/js/error-handler.js +226 -0
- webview/static/js/ethers.min.js +1 -0
- webview/static/js/event-filter.js +137 -0
- webview/static/js/event-store.js +351 -0
- webview/static/js/file-attachments.js +374 -0
- webview/static/js/file-loader.js +163 -0
- webview/static/js/index.js +103 -0
- webview/static/js/performance-utils.js +196 -0
- webview/static/js/profile.js +396 -0
- webview/static/js/screenshot.js +386 -0
- webview/static/js/session.js +1527 -0
- webview/static/js/settings.js +1288 -0
- webview/static/js/sidebar-data.js +766 -0
- webview/static/js/sidebar-toggle.js +296 -0
- webview/static/js/socket.io.min.js +7 -0
- webview/static/js/stats.js +355 -0
- webview/static/js/ui-utils.js +420 -0
- webview/static/js/workspace.js +966 -0
- webview/stats_service.py +613 -0
- webview/templates/__init__.py +1 -0
- webview/templates/__pycache__/__init__.cpython-311.pyc +0 -0
- webview/templates/activity.html +36 -0
- webview/templates/admin/activity.html +122 -0
- webview/templates/admin/dashboard.html +112 -0
- webview/templates/admin/user_detail.html +228 -0
- webview/templates/admin/users.html +105 -0
- webview/templates/autonomy.html +60 -0
- webview/templates/error.html +133 -0
- webview/templates/finance.html +90 -0
- webview/templates/identity.html +73 -0
- webview/templates/index.html +42 -0
- webview/templates/layout.html +125 -0
- webview/templates/memory.html +59 -0
- webview/templates/owner_login.html +15 -0
- webview/templates/profile.html +121 -0
- webview/templates/session.html +312 -0
- webview/templates/settings.html +534 -0
- webview/templates/sidebar.html +112 -0
- webview/templates/signin.html +213 -0
- webview/templates/status.html +14 -0
- webview/templates/system.html +45 -0
- webview/webgate.py +196 -0
|
@@ -0,0 +1,899 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Service for managing per-user MCP server configurations.
|
|
3
|
+
|
|
4
|
+
Provides high-level operations for adding, configuring, and managing
|
|
5
|
+
user-specific MCP servers with validation and security checks.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import time
|
|
9
|
+
from typing import List, Optional, Dict, Any
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
|
|
12
|
+
from modules.database.user_mcp_servers import (
|
|
13
|
+
UserMCPServersHandler,
|
|
14
|
+
UserMCPServer,
|
|
15
|
+
UserMCPSettings
|
|
16
|
+
)
|
|
17
|
+
from tools.mcp.security import MCPEncryption, MCPURLValidator, get_encryption, get_url_validator
|
|
18
|
+
from tools.mcp.config import MCPServerConfig, MCPServerType
|
|
19
|
+
|
|
20
|
+
from core.logging import get_component_logger
|
|
21
|
+
|
|
22
|
+
# Module-level logger for static methods
|
|
23
|
+
logger = get_component_logger("UserMCPService")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class AddServerResult:
|
|
28
|
+
"""Result of adding a server."""
|
|
29
|
+
|
|
30
|
+
success: bool
|
|
31
|
+
server: Optional[UserMCPServer] = None
|
|
32
|
+
error: Optional[str] = None
|
|
33
|
+
ready: bool = False
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class TestConnectionResult:
|
|
38
|
+
"""Result of testing server connection."""
|
|
39
|
+
|
|
40
|
+
success: bool
|
|
41
|
+
latency_ms: Optional[float] = None
|
|
42
|
+
error: Optional[str] = None
|
|
43
|
+
tools_discovered: Optional[int] = None
|
|
44
|
+
tools: Optional[List[str]] = None # List of tool names discovered
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class RateLimiter:
|
|
48
|
+
"""Simple in-memory rate limiter (FIX #11)."""
|
|
49
|
+
|
|
50
|
+
def __init__(self, max_requests: int = 10, window_seconds: int = 60):
|
|
51
|
+
self.max_requests = max_requests
|
|
52
|
+
self.window_seconds = window_seconds
|
|
53
|
+
self._requests: Dict[str, List[float]] = {} # user_id -> list of timestamps
|
|
54
|
+
|
|
55
|
+
def check(self, user_id: str) -> bool:
|
|
56
|
+
"""Check if request is allowed. Returns True if allowed."""
|
|
57
|
+
import time
|
|
58
|
+
now = time.time()
|
|
59
|
+
|
|
60
|
+
if user_id not in self._requests:
|
|
61
|
+
self._requests[user_id] = []
|
|
62
|
+
|
|
63
|
+
# Remove old requests outside window
|
|
64
|
+
cutoff = now - self.window_seconds
|
|
65
|
+
self._requests[user_id] = [t for t in self._requests[user_id] if t > cutoff]
|
|
66
|
+
|
|
67
|
+
if len(self._requests[user_id]) >= self.max_requests:
|
|
68
|
+
return False
|
|
69
|
+
|
|
70
|
+
self._requests[user_id].append(now)
|
|
71
|
+
return True
|
|
72
|
+
|
|
73
|
+
def remaining(self, user_id: str) -> int:
|
|
74
|
+
"""Get remaining requests in window."""
|
|
75
|
+
import time
|
|
76
|
+
now = time.time()
|
|
77
|
+
cutoff = now - self.window_seconds
|
|
78
|
+
|
|
79
|
+
if user_id not in self._requests:
|
|
80
|
+
return self.max_requests
|
|
81
|
+
|
|
82
|
+
recent = [t for t in self._requests[user_id] if t > cutoff]
|
|
83
|
+
return max(0, self.max_requests - len(recent))
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class UserMCPService:
|
|
87
|
+
"""Service for user MCP server management."""
|
|
88
|
+
|
|
89
|
+
def __init__(
|
|
90
|
+
self,
|
|
91
|
+
db_handler: UserMCPServersHandler,
|
|
92
|
+
encryption: Optional[MCPEncryption] = None,
|
|
93
|
+
url_validator: Optional[MCPURLValidator] = None,
|
|
94
|
+
allow_http: bool = False,
|
|
95
|
+
rate_limit_requests: int = 20,
|
|
96
|
+
rate_limit_window: int = 60
|
|
97
|
+
):
|
|
98
|
+
"""
|
|
99
|
+
Initialize service.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
db_handler: Database handler for user MCP servers
|
|
103
|
+
encryption: Encryption instance (uses default if not provided)
|
|
104
|
+
url_validator: URL validator (uses default if not provided)
|
|
105
|
+
allow_http: Allow HTTP URLs (development only)
|
|
106
|
+
rate_limit_requests: Max requests per window (FIX #11)
|
|
107
|
+
rate_limit_window: Rate limit window in seconds (FIX #11)
|
|
108
|
+
"""
|
|
109
|
+
self.db = db_handler
|
|
110
|
+
self.encryption = encryption or get_encryption()
|
|
111
|
+
self.validator = url_validator or get_url_validator(allow_http=allow_http)
|
|
112
|
+
self.logger = get_component_logger("UserMCPService")
|
|
113
|
+
# FIX #11: Rate limiting
|
|
114
|
+
self._rate_limiter = RateLimiter(rate_limit_requests, rate_limit_window)
|
|
115
|
+
|
|
116
|
+
def _check_rate_limit(self, user_id: str) -> None:
|
|
117
|
+
"""Check rate limit and raise if exceeded (FIX #11)."""
|
|
118
|
+
if not self._rate_limiter.check(user_id):
|
|
119
|
+
remaining = self._rate_limiter.remaining(user_id)
|
|
120
|
+
raise ValueError(
|
|
121
|
+
f"Rate limit exceeded. Please wait before making more requests. "
|
|
122
|
+
f"({remaining} requests remaining)"
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
@staticmethod
|
|
126
|
+
def _sanitize_error(error: str) -> str:
|
|
127
|
+
"""Sanitize error message to hide internal details (FIX #14)."""
|
|
128
|
+
import re
|
|
129
|
+
# Remove file paths
|
|
130
|
+
error = re.sub(r'/[^\s:]+\.py', '[internal]', error)
|
|
131
|
+
error = re.sub(r'line \d+', '', error)
|
|
132
|
+
# Remove stack trace indicators
|
|
133
|
+
error = re.sub(r'Traceback.*?:', '', error, flags=re.DOTALL)
|
|
134
|
+
error = re.sub(r'File "[^"]+",', '', error)
|
|
135
|
+
# Remove internal module names
|
|
136
|
+
error = re.sub(r'\b(tools|modules|agents)\.[a-z_.]+', '[internal]', error)
|
|
137
|
+
# Limit length
|
|
138
|
+
if len(error) > 200:
|
|
139
|
+
error = error[:200] + '...'
|
|
140
|
+
return error.strip()
|
|
141
|
+
|
|
142
|
+
async def add_server(
|
|
143
|
+
self,
|
|
144
|
+
user_id: str,
|
|
145
|
+
server_name: str,
|
|
146
|
+
server_url: str,
|
|
147
|
+
server_type: str = "sse",
|
|
148
|
+
api_key: Optional[str] = None,
|
|
149
|
+
headers: Optional[Dict[str, str]] = None,
|
|
150
|
+
auth_method: str = "api_key",
|
|
151
|
+
display_name: Optional[str] = None,
|
|
152
|
+
verify_connection: bool = False,
|
|
153
|
+
**options
|
|
154
|
+
) -> AddServerResult:
|
|
155
|
+
"""
|
|
156
|
+
Add a new MCP server for a user.
|
|
157
|
+
|
|
158
|
+
Performs validation and security checks before adding.
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
user_id: Owner's user ID
|
|
162
|
+
server_name: Unique name for the server (lowercase, alphanumeric, hyphens)
|
|
163
|
+
server_url: Server endpoint URL (must be HTTPS)
|
|
164
|
+
server_type: 'sse' or 'http' (NOT 'stdio' - blocked for security)
|
|
165
|
+
api_key: API key for authentication
|
|
166
|
+
headers: Custom headers for requests
|
|
167
|
+
auth_method: 'api_key', 'bearer', or 'none'
|
|
168
|
+
display_name: Friendly display name
|
|
169
|
+
verify_connection: If True, verify MCP protocol before saving (FIX #6)
|
|
170
|
+
**options: Additional options (timeout, retry_attempts, etc.)
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
AddServerResult with success status and server or error
|
|
174
|
+
"""
|
|
175
|
+
# FIX #11: Check rate limit
|
|
176
|
+
try:
|
|
177
|
+
self._check_rate_limit(user_id)
|
|
178
|
+
except ValueError as e:
|
|
179
|
+
return AddServerResult(success=False, error=str(e))
|
|
180
|
+
|
|
181
|
+
# Validate server type (NO STDIO for users)
|
|
182
|
+
type_valid, type_error = self.validator.validate_server_type(server_type)
|
|
183
|
+
if not type_valid:
|
|
184
|
+
return AddServerResult(
|
|
185
|
+
success=False,
|
|
186
|
+
error=type_error
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
# Validate URL (HTTPS only, no internal IPs)
|
|
190
|
+
url_valid, url_error = self.validator.validate(server_url)
|
|
191
|
+
if not url_valid:
|
|
192
|
+
return AddServerResult(
|
|
193
|
+
success=False,
|
|
194
|
+
error=f"Invalid URL: {url_error}"
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
# Validate server name format
|
|
198
|
+
if not self._validate_server_name(server_name):
|
|
199
|
+
return AddServerResult(
|
|
200
|
+
success=False,
|
|
201
|
+
error="Server name must be lowercase, alphanumeric with hyphens only (3-50 chars)"
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
# Check user's server limit
|
|
205
|
+
settings = await self.db.get_user_settings(user_id)
|
|
206
|
+
current_count = await self.db.server_count(user_id)
|
|
207
|
+
|
|
208
|
+
if current_count >= settings.max_servers:
|
|
209
|
+
return AddServerResult(
|
|
210
|
+
success=False,
|
|
211
|
+
error=f"Maximum server limit ({settings.max_servers}) reached"
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
# Check for duplicate name
|
|
215
|
+
existing = await self.db.get_server(user_id, server_name)
|
|
216
|
+
if existing:
|
|
217
|
+
return AddServerResult(
|
|
218
|
+
success=False,
|
|
219
|
+
error=f"Server '{server_name}' already exists"
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
# FIX #6: Optionally verify connection before saving
|
|
223
|
+
tools_discovered = 0
|
|
224
|
+
if verify_connection and (api_key is not None or auth_method == 'none'):
|
|
225
|
+
# Build temporary config for testing
|
|
226
|
+
temp_config = MCPServerConfig(
|
|
227
|
+
type=MCPServerType(server_type),
|
|
228
|
+
url=server_url,
|
|
229
|
+
headers=self._build_auth_headers(headers, api_key, auth_method),
|
|
230
|
+
enabled=True,
|
|
231
|
+
timeout=options.get('timeout', 30)
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
# Test MCP protocol
|
|
235
|
+
test_result = await self._test_mcp_protocol_with_config(temp_config)
|
|
236
|
+
|
|
237
|
+
if not test_result.success:
|
|
238
|
+
return AddServerResult(
|
|
239
|
+
success=False,
|
|
240
|
+
error=f"Server verification failed: {test_result.error}"
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
tools_discovered = test_result.tools_discovered or 0
|
|
244
|
+
self.logger.info(f"Server {server_name} verified: {tools_discovered} tools discovered")
|
|
245
|
+
|
|
246
|
+
# Add server
|
|
247
|
+
try:
|
|
248
|
+
server = await self.db.add_server(
|
|
249
|
+
user_id=user_id,
|
|
250
|
+
server_name=server_name,
|
|
251
|
+
server_url=server_url,
|
|
252
|
+
server_type=server_type,
|
|
253
|
+
auth_method=auth_method,
|
|
254
|
+
api_key=api_key,
|
|
255
|
+
headers=headers,
|
|
256
|
+
display_name=display_name,
|
|
257
|
+
**options
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
# Update tools_discovered if we verified
|
|
261
|
+
if tools_discovered > 0:
|
|
262
|
+
await self.db.update_connection_status(
|
|
263
|
+
user_id, server_name,
|
|
264
|
+
connected=True,
|
|
265
|
+
tools_count=tools_discovered
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
return AddServerResult(
|
|
269
|
+
success=True,
|
|
270
|
+
server=server,
|
|
271
|
+
ready=api_key is not None or auth_method == 'none'
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
except Exception as e:
|
|
275
|
+
self.logger.error(f"Failed to add server '{server_name}' for user {user_id}: {e}")
|
|
276
|
+
return AddServerResult(
|
|
277
|
+
success=False,
|
|
278
|
+
error=f"Database error: {str(e)}"
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
def _build_auth_headers(
|
|
282
|
+
self,
|
|
283
|
+
headers: Optional[Dict[str, str]],
|
|
284
|
+
api_key: Optional[str],
|
|
285
|
+
auth_method: str
|
|
286
|
+
) -> Optional[Dict[str, str]]:
|
|
287
|
+
"""Build headers dict with authentication."""
|
|
288
|
+
final_headers = dict(headers) if headers else {}
|
|
289
|
+
if api_key:
|
|
290
|
+
if auth_method == 'bearer':
|
|
291
|
+
final_headers['Authorization'] = f'Bearer {api_key}'
|
|
292
|
+
elif auth_method == 'api_key':
|
|
293
|
+
final_headers['Authorization'] = f'Bearer {api_key}'
|
|
294
|
+
return final_headers or None
|
|
295
|
+
|
|
296
|
+
async def _test_mcp_protocol_with_config(
|
|
297
|
+
self,
|
|
298
|
+
config: MCPServerConfig
|
|
299
|
+
) -> TestConnectionResult:
|
|
300
|
+
"""Test MCP protocol with a config object (for verification before saving)."""
|
|
301
|
+
import time
|
|
302
|
+
import asyncio
|
|
303
|
+
from tools.mcp.protocol import MCPClient, MCPSSETransport, MCPHTTPTransport
|
|
304
|
+
|
|
305
|
+
client = None
|
|
306
|
+
start = time.time()
|
|
307
|
+
|
|
308
|
+
try:
|
|
309
|
+
if config.type == MCPServerType.SSE:
|
|
310
|
+
transport = MCPSSETransport(
|
|
311
|
+
url=config.url,
|
|
312
|
+
headers=config.headers,
|
|
313
|
+
timeout=min(config.timeout, 30),
|
|
314
|
+
validate_ssrf=True # SSRF: user-supplied URL
|
|
315
|
+
)
|
|
316
|
+
elif config.type == MCPServerType.HTTP:
|
|
317
|
+
transport = MCPHTTPTransport(
|
|
318
|
+
url=config.url,
|
|
319
|
+
headers=config.headers,
|
|
320
|
+
timeout=min(config.timeout, 30),
|
|
321
|
+
validate_ssrf=True # SSRF: user-supplied URL
|
|
322
|
+
)
|
|
323
|
+
else:
|
|
324
|
+
return TestConnectionResult(
|
|
325
|
+
success=False,
|
|
326
|
+
error=f"Unsupported server type: {config.type}"
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
client = MCPClient(transport)
|
|
330
|
+
|
|
331
|
+
try:
|
|
332
|
+
await asyncio.wait_for(client.connect(), timeout=30)
|
|
333
|
+
except asyncio.TimeoutError:
|
|
334
|
+
return TestConnectionResult(
|
|
335
|
+
success=False,
|
|
336
|
+
error="MCP handshake timed out (30s)"
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
latency = (time.time() - start) * 1000
|
|
340
|
+
tools = client.tools
|
|
341
|
+
tool_names = [t.get('name', 'unknown') for t in tools]
|
|
342
|
+
|
|
343
|
+
return TestConnectionResult(
|
|
344
|
+
success=True,
|
|
345
|
+
latency_ms=round(latency, 2),
|
|
346
|
+
tools_discovered=len(tools),
|
|
347
|
+
tools=tool_names[:20]
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
except Exception as e:
|
|
351
|
+
return TestConnectionResult(
|
|
352
|
+
success=False,
|
|
353
|
+
error=f"MCP protocol error: {str(e)}"
|
|
354
|
+
)
|
|
355
|
+
finally:
|
|
356
|
+
if client:
|
|
357
|
+
try:
|
|
358
|
+
await client.close()
|
|
359
|
+
except Exception:
|
|
360
|
+
pass
|
|
361
|
+
|
|
362
|
+
async def get_user_servers(
|
|
363
|
+
self,
|
|
364
|
+
user_id: str,
|
|
365
|
+
enabled_only: bool = True
|
|
366
|
+
) -> List[UserMCPServer]:
|
|
367
|
+
"""
|
|
368
|
+
Get all MCP servers for a user.
|
|
369
|
+
|
|
370
|
+
Args:
|
|
371
|
+
user_id: Owner's user ID
|
|
372
|
+
enabled_only: Only return enabled servers
|
|
373
|
+
|
|
374
|
+
Returns:
|
|
375
|
+
List of UserMCPServer records
|
|
376
|
+
"""
|
|
377
|
+
return await self.db.get_user_servers(user_id, enabled_only)
|
|
378
|
+
|
|
379
|
+
async def get_server(
|
|
380
|
+
self,
|
|
381
|
+
user_id: str,
|
|
382
|
+
server_name: str
|
|
383
|
+
) -> Optional[UserMCPServer]:
|
|
384
|
+
"""
|
|
385
|
+
Get a specific server.
|
|
386
|
+
|
|
387
|
+
Args:
|
|
388
|
+
user_id: Owner's user ID
|
|
389
|
+
server_name: Server name
|
|
390
|
+
|
|
391
|
+
Returns:
|
|
392
|
+
UserMCPServer or None
|
|
393
|
+
"""
|
|
394
|
+
return await self.db.get_server(user_id, server_name)
|
|
395
|
+
|
|
396
|
+
async def get_server_config(
|
|
397
|
+
self,
|
|
398
|
+
user_id: str,
|
|
399
|
+
server_name: str
|
|
400
|
+
) -> Optional[MCPServerConfig]:
|
|
401
|
+
"""
|
|
402
|
+
Get MCPServerConfig for a user's server (for use by MCPServerManager).
|
|
403
|
+
|
|
404
|
+
Decrypts credentials and builds config object.
|
|
405
|
+
|
|
406
|
+
Args:
|
|
407
|
+
user_id: Owner's user ID
|
|
408
|
+
server_name: Server name
|
|
409
|
+
|
|
410
|
+
Returns:
|
|
411
|
+
MCPServerConfig ready for connection, or None
|
|
412
|
+
"""
|
|
413
|
+
server = await self.db.get_server(user_id, server_name)
|
|
414
|
+
if not server:
|
|
415
|
+
return None
|
|
416
|
+
|
|
417
|
+
# Get decrypted credentials
|
|
418
|
+
api_key, headers = await self.db.get_decrypted_credentials(user_id, server_name)
|
|
419
|
+
|
|
420
|
+
# Build headers with auth
|
|
421
|
+
final_headers = headers or {}
|
|
422
|
+
if api_key:
|
|
423
|
+
if server.auth_method == 'bearer':
|
|
424
|
+
final_headers['Authorization'] = f'Bearer {api_key}'
|
|
425
|
+
elif server.auth_method == 'api_key':
|
|
426
|
+
# Default to Bearer, but could be customized per server
|
|
427
|
+
final_headers['Authorization'] = f'Bearer {api_key}'
|
|
428
|
+
|
|
429
|
+
return MCPServerConfig(
|
|
430
|
+
type=MCPServerType(server.server_type),
|
|
431
|
+
url=server.server_url,
|
|
432
|
+
headers=final_headers or None,
|
|
433
|
+
enabled=server.enabled,
|
|
434
|
+
timeout=server.timeout,
|
|
435
|
+
retry_attempts=server.retry_attempts,
|
|
436
|
+
retry_delay=server.retry_delay,
|
|
437
|
+
auto_reconnect=server.auto_reconnect,
|
|
438
|
+
max_concurrent_requests=server.max_concurrent_requests,
|
|
439
|
+
message_endpoint=server.message_endpoint # For SSE servers
|
|
440
|
+
)
|
|
441
|
+
|
|
442
|
+
async def update_server(
|
|
443
|
+
self,
|
|
444
|
+
user_id: str,
|
|
445
|
+
server_name: str,
|
|
446
|
+
**updates
|
|
447
|
+
) -> bool:
|
|
448
|
+
"""
|
|
449
|
+
Update server configuration.
|
|
450
|
+
|
|
451
|
+
Args:
|
|
452
|
+
user_id: Owner's user ID
|
|
453
|
+
server_name: Server name
|
|
454
|
+
**updates: Fields to update
|
|
455
|
+
|
|
456
|
+
Returns:
|
|
457
|
+
True if updated successfully
|
|
458
|
+
"""
|
|
459
|
+
# Validate URL if being updated
|
|
460
|
+
if 'server_url' in updates:
|
|
461
|
+
url_valid, url_error = self.validator.validate(updates['server_url'])
|
|
462
|
+
if not url_valid:
|
|
463
|
+
raise ValueError(f"Invalid URL: {url_error}")
|
|
464
|
+
|
|
465
|
+
# Validate server_type if being updated
|
|
466
|
+
if 'server_type' in updates:
|
|
467
|
+
type_valid, type_error = self.validator.validate_server_type(updates['server_type'])
|
|
468
|
+
if not type_valid:
|
|
469
|
+
raise ValueError(type_error)
|
|
470
|
+
|
|
471
|
+
return await self.db.update_server(user_id, server_name, **updates)
|
|
472
|
+
|
|
473
|
+
async def delete_server(self, user_id: str, server_name: str) -> bool:
|
|
474
|
+
"""
|
|
475
|
+
Delete a user's MCP server.
|
|
476
|
+
|
|
477
|
+
Args:
|
|
478
|
+
user_id: Owner's user ID
|
|
479
|
+
server_name: Server name
|
|
480
|
+
|
|
481
|
+
Returns:
|
|
482
|
+
True if deleted successfully
|
|
483
|
+
"""
|
|
484
|
+
# FIX #11: Check rate limit
|
|
485
|
+
self._check_rate_limit(user_id)
|
|
486
|
+
|
|
487
|
+
# FIX #12: Audit log (uses private method)
|
|
488
|
+
await self.db._audit_log(
|
|
489
|
+
user_id=user_id,
|
|
490
|
+
action="server_deleted",
|
|
491
|
+
server_name=server_name,
|
|
492
|
+
details=None
|
|
493
|
+
)
|
|
494
|
+
|
|
495
|
+
return await self.db.delete_server(user_id, server_name)
|
|
496
|
+
|
|
497
|
+
async def test_connection(
|
|
498
|
+
self,
|
|
499
|
+
user_id: str,
|
|
500
|
+
server_name: str,
|
|
501
|
+
verify_protocol: bool = True
|
|
502
|
+
) -> TestConnectionResult:
|
|
503
|
+
"""
|
|
504
|
+
Test connectivity to a user's MCP server.
|
|
505
|
+
|
|
506
|
+
Performs actual MCP protocol handshake to verify the server speaks MCP.
|
|
507
|
+
|
|
508
|
+
Args:
|
|
509
|
+
user_id: Owner's user ID
|
|
510
|
+
server_name: Server name
|
|
511
|
+
verify_protocol: If True, performs full MCP handshake (default).
|
|
512
|
+
If False, only checks HTTP connectivity.
|
|
513
|
+
|
|
514
|
+
Returns:
|
|
515
|
+
TestConnectionResult with status and discovered tools
|
|
516
|
+
"""
|
|
517
|
+
# FIX #11: Check rate limit
|
|
518
|
+
try:
|
|
519
|
+
self._check_rate_limit(user_id)
|
|
520
|
+
except ValueError as e:
|
|
521
|
+
return TestConnectionResult(success=False, error=str(e))
|
|
522
|
+
|
|
523
|
+
config = await self.get_server_config(user_id, server_name)
|
|
524
|
+
if not config:
|
|
525
|
+
return TestConnectionResult(
|
|
526
|
+
success=False,
|
|
527
|
+
error="Server not found"
|
|
528
|
+
)
|
|
529
|
+
|
|
530
|
+
try:
|
|
531
|
+
start = time.time()
|
|
532
|
+
|
|
533
|
+
if verify_protocol:
|
|
534
|
+
# FIX #5: Perform actual MCP protocol handshake
|
|
535
|
+
result = await self._test_mcp_protocol(user_id, server_name, config, start)
|
|
536
|
+
else:
|
|
537
|
+
# Legacy: Just check HTTP connectivity
|
|
538
|
+
result = await self._test_http_connectivity(user_id, server_name, config, start)
|
|
539
|
+
|
|
540
|
+
# FIX #12: Audit log test results (uses private method)
|
|
541
|
+
await self.db._audit_log(
|
|
542
|
+
user_id=user_id,
|
|
543
|
+
action="server_tested",
|
|
544
|
+
server_name=server_name,
|
|
545
|
+
details={"success": result.success, "tools": result.tools_discovered}
|
|
546
|
+
)
|
|
547
|
+
|
|
548
|
+
return result
|
|
549
|
+
|
|
550
|
+
except Exception as e:
|
|
551
|
+
# FIX #14: Sanitize error message
|
|
552
|
+
error = self._sanitize_error(f"Unexpected error: {str(e)}")
|
|
553
|
+
await self.db.update_connection_status(
|
|
554
|
+
user_id, server_name,
|
|
555
|
+
connected=False,
|
|
556
|
+
error=error
|
|
557
|
+
)
|
|
558
|
+
return TestConnectionResult(
|
|
559
|
+
success=False,
|
|
560
|
+
error=error
|
|
561
|
+
)
|
|
562
|
+
|
|
563
|
+
async def _test_http_connectivity(
|
|
564
|
+
self,
|
|
565
|
+
user_id: str,
|
|
566
|
+
server_name: str,
|
|
567
|
+
config: MCPServerConfig,
|
|
568
|
+
start_time: float
|
|
569
|
+
) -> TestConnectionResult:
|
|
570
|
+
"""Test basic HTTP connectivity (legacy method)."""
|
|
571
|
+
import aiohttp
|
|
572
|
+
import time
|
|
573
|
+
|
|
574
|
+
try:
|
|
575
|
+
async with aiohttp.ClientSession() as session:
|
|
576
|
+
async with session.get(
|
|
577
|
+
config.url,
|
|
578
|
+
headers=config.headers,
|
|
579
|
+
timeout=aiohttp.ClientTimeout(total=10)
|
|
580
|
+
) as resp:
|
|
581
|
+
latency = (time.time() - start_time) * 1000
|
|
582
|
+
|
|
583
|
+
if resp.status < 400:
|
|
584
|
+
await self.db.update_connection_status(
|
|
585
|
+
user_id, server_name, connected=True
|
|
586
|
+
)
|
|
587
|
+
return TestConnectionResult(
|
|
588
|
+
success=True,
|
|
589
|
+
latency_ms=round(latency, 2)
|
|
590
|
+
)
|
|
591
|
+
else:
|
|
592
|
+
error = f"Server returned {resp.status}"
|
|
593
|
+
await self.db.update_connection_status(
|
|
594
|
+
user_id, server_name, connected=False, error=error
|
|
595
|
+
)
|
|
596
|
+
return TestConnectionResult(success=False, error=error)
|
|
597
|
+
|
|
598
|
+
except aiohttp.ClientError as e:
|
|
599
|
+
error = f"Connection error: {str(e)}"
|
|
600
|
+
await self.db.update_connection_status(
|
|
601
|
+
user_id, server_name, connected=False, error=error
|
|
602
|
+
)
|
|
603
|
+
return TestConnectionResult(success=False, error=error)
|
|
604
|
+
|
|
605
|
+
async def _test_mcp_protocol(
|
|
606
|
+
self,
|
|
607
|
+
user_id: str,
|
|
608
|
+
server_name: str,
|
|
609
|
+
config: MCPServerConfig,
|
|
610
|
+
start_time: float
|
|
611
|
+
) -> TestConnectionResult:
|
|
612
|
+
"""
|
|
613
|
+
Test MCP protocol by performing actual handshake.
|
|
614
|
+
|
|
615
|
+
Connects, sends initialize request, and attempts to list tools.
|
|
616
|
+
"""
|
|
617
|
+
import time
|
|
618
|
+
import asyncio
|
|
619
|
+
from tools.mcp.protocol import MCPClient, MCPSSETransport, MCPHTTPTransport
|
|
620
|
+
|
|
621
|
+
client = None
|
|
622
|
+
try:
|
|
623
|
+
# Create appropriate transport based on server type
|
|
624
|
+
if config.type == MCPServerType.SSE:
|
|
625
|
+
transport = MCPSSETransport(
|
|
626
|
+
url=config.url,
|
|
627
|
+
headers=config.headers,
|
|
628
|
+
timeout=min(config.timeout, 30), # Cap at 30s for test
|
|
629
|
+
validate_ssrf=True # SSRF: user-supplied URL
|
|
630
|
+
)
|
|
631
|
+
elif config.type == MCPServerType.HTTP:
|
|
632
|
+
transport = MCPHTTPTransport(
|
|
633
|
+
url=config.url,
|
|
634
|
+
headers=config.headers,
|
|
635
|
+
timeout=min(config.timeout, 30),
|
|
636
|
+
validate_ssrf=True # SSRF: user-supplied URL
|
|
637
|
+
)
|
|
638
|
+
else:
|
|
639
|
+
return TestConnectionResult(
|
|
640
|
+
success=False,
|
|
641
|
+
error=f"Unsupported server type for testing: {config.type}"
|
|
642
|
+
)
|
|
643
|
+
|
|
644
|
+
# Create client and connect with timeout
|
|
645
|
+
client = MCPClient(transport)
|
|
646
|
+
|
|
647
|
+
try:
|
|
648
|
+
await asyncio.wait_for(client.connect(), timeout=30)
|
|
649
|
+
except asyncio.TimeoutError:
|
|
650
|
+
return TestConnectionResult(
|
|
651
|
+
success=False,
|
|
652
|
+
error="MCP handshake timed out (30s)"
|
|
653
|
+
)
|
|
654
|
+
|
|
655
|
+
latency = (time.time() - start_time) * 1000
|
|
656
|
+
|
|
657
|
+
# Get discovered tools
|
|
658
|
+
tools = client.tools
|
|
659
|
+
tool_names = [t.get('name', 'unknown') for t in tools]
|
|
660
|
+
tools_count = len(tools)
|
|
661
|
+
|
|
662
|
+
# Update database with results
|
|
663
|
+
await self.db.update_connection_status(
|
|
664
|
+
user_id, server_name,
|
|
665
|
+
connected=True,
|
|
666
|
+
tools_count=tools_count
|
|
667
|
+
)
|
|
668
|
+
|
|
669
|
+
self.logger.info(
|
|
670
|
+
f"MCP protocol test passed for {server_name}: "
|
|
671
|
+
f"{tools_count} tools, {latency:.0f}ms"
|
|
672
|
+
)
|
|
673
|
+
|
|
674
|
+
return TestConnectionResult(
|
|
675
|
+
success=True,
|
|
676
|
+
latency_ms=round(latency, 2),
|
|
677
|
+
tools_discovered=tools_count,
|
|
678
|
+
tools=tool_names[:20] # Limit to first 20 tool names
|
|
679
|
+
)
|
|
680
|
+
|
|
681
|
+
except Exception as e:
|
|
682
|
+
error = f"MCP protocol error: {str(e)}"
|
|
683
|
+
await self.db.update_connection_status(
|
|
684
|
+
user_id, server_name,
|
|
685
|
+
connected=False,
|
|
686
|
+
error=error
|
|
687
|
+
)
|
|
688
|
+
return TestConnectionResult(success=False, error=error)
|
|
689
|
+
|
|
690
|
+
finally:
|
|
691
|
+
# Clean up client
|
|
692
|
+
if client:
|
|
693
|
+
try:
|
|
694
|
+
await client.close()
|
|
695
|
+
except Exception:
|
|
696
|
+
pass
|
|
697
|
+
|
|
698
|
+
async def get_user_settings(self, user_id: str) -> UserMCPSettings:
|
|
699
|
+
"""
|
|
700
|
+
Get user's MCP settings.
|
|
701
|
+
|
|
702
|
+
Args:
|
|
703
|
+
user_id: User ID
|
|
704
|
+
|
|
705
|
+
Returns:
|
|
706
|
+
UserMCPSettings record
|
|
707
|
+
"""
|
|
708
|
+
return await self.db.get_user_settings(user_id)
|
|
709
|
+
|
|
710
|
+
async def update_user_settings(self, user_id: str, **updates) -> bool:
|
|
711
|
+
"""
|
|
712
|
+
Update user's MCP settings.
|
|
713
|
+
|
|
714
|
+
Args:
|
|
715
|
+
user_id: User ID
|
|
716
|
+
**updates: Settings to update
|
|
717
|
+
|
|
718
|
+
Returns:
|
|
719
|
+
True if updated
|
|
720
|
+
"""
|
|
721
|
+
return await self.db.update_user_settings(user_id, **updates)
|
|
722
|
+
|
|
723
|
+
async def get_available_servers_for_session(
|
|
724
|
+
self,
|
|
725
|
+
user_id: str,
|
|
726
|
+
include_global: bool = True
|
|
727
|
+
) -> Dict[str, Any]:
|
|
728
|
+
"""
|
|
729
|
+
Get all available MCP servers for a session.
|
|
730
|
+
|
|
731
|
+
Returns both global (platform) servers and user's custom servers.
|
|
732
|
+
|
|
733
|
+
Args:
|
|
734
|
+
user_id: User ID
|
|
735
|
+
include_global: Whether to include global servers
|
|
736
|
+
|
|
737
|
+
Returns:
|
|
738
|
+
Dict with 'global' and 'user' server lists
|
|
739
|
+
"""
|
|
740
|
+
settings = await self.db.get_user_settings(user_id)
|
|
741
|
+
|
|
742
|
+
result = {
|
|
743
|
+
'global': [],
|
|
744
|
+
'user': []
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
# Get user's custom servers
|
|
748
|
+
user_servers = await self.db.get_user_servers(user_id, enabled_only=True)
|
|
749
|
+
for server in user_servers:
|
|
750
|
+
result['user'].append({
|
|
751
|
+
'name': server.server_name,
|
|
752
|
+
'tool_id': f"mcp:user:{server.server_name}",
|
|
753
|
+
'display_name': server.display_name or server.server_name,
|
|
754
|
+
'type': server.server_type,
|
|
755
|
+
'status': server.auth_status,
|
|
756
|
+
'tools_count': server.tools_discovered
|
|
757
|
+
})
|
|
758
|
+
|
|
759
|
+
# Include global servers if enabled
|
|
760
|
+
if include_global and settings.include_global_servers:
|
|
761
|
+
# Global servers are loaded from mcp_config.json at startup
|
|
762
|
+
# This will be populated by MCPTool when it initializes
|
|
763
|
+
result['include_global'] = True
|
|
764
|
+
else:
|
|
765
|
+
result['include_global'] = False
|
|
766
|
+
|
|
767
|
+
return result
|
|
768
|
+
|
|
769
|
+
async def health_check_user_servers(
|
|
770
|
+
self,
|
|
771
|
+
user_id: str,
|
|
772
|
+
update_status: bool = True
|
|
773
|
+
) -> Dict[str, Any]:
|
|
774
|
+
"""
|
|
775
|
+
Perform health check on all user's MCP servers (FIX #15).
|
|
776
|
+
|
|
777
|
+
Args:
|
|
778
|
+
user_id: User ID to check
|
|
779
|
+
update_status: If True, update connection status in database
|
|
780
|
+
|
|
781
|
+
Returns:
|
|
782
|
+
Dict with health status for each server
|
|
783
|
+
"""
|
|
784
|
+
servers = await self.get_user_servers(user_id, enabled_only=True)
|
|
785
|
+
results = {
|
|
786
|
+
"user_id": user_id,
|
|
787
|
+
"checked_at": time.strftime("%Y-%m-%d %H:%M:%S"),
|
|
788
|
+
"total_servers": len(servers),
|
|
789
|
+
"healthy": 0,
|
|
790
|
+
"unhealthy": 0,
|
|
791
|
+
"servers": {}
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
for server in servers:
|
|
795
|
+
try:
|
|
796
|
+
# Use fast HTTP check for health (not full MCP protocol)
|
|
797
|
+
test_result = await self.test_connection(
|
|
798
|
+
user_id,
|
|
799
|
+
server.server_name,
|
|
800
|
+
verify_protocol=False # Faster check
|
|
801
|
+
)
|
|
802
|
+
|
|
803
|
+
if test_result.success:
|
|
804
|
+
results["healthy"] += 1
|
|
805
|
+
results["servers"][server.server_name] = {
|
|
806
|
+
"status": "healthy",
|
|
807
|
+
"latency_ms": test_result.latency_ms
|
|
808
|
+
}
|
|
809
|
+
else:
|
|
810
|
+
results["unhealthy"] += 1
|
|
811
|
+
results["servers"][server.server_name] = {
|
|
812
|
+
"status": "unhealthy",
|
|
813
|
+
"error": self._sanitize_error(test_result.error or "Unknown error")
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
except Exception as e:
|
|
817
|
+
results["unhealthy"] += 1
|
|
818
|
+
results["servers"][server.server_name] = {
|
|
819
|
+
"status": "error",
|
|
820
|
+
"error": self._sanitize_error(str(e))
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
return results
|
|
824
|
+
|
|
825
|
+
def _validate_server_name(self, name: str) -> bool:
|
|
826
|
+
"""
|
|
827
|
+
Validate server name format.
|
|
828
|
+
|
|
829
|
+
Must be:
|
|
830
|
+
- 3-50 characters
|
|
831
|
+
- Lowercase
|
|
832
|
+
- Alphanumeric and hyphens only
|
|
833
|
+
- No leading/trailing hyphens
|
|
834
|
+
|
|
835
|
+
Args:
|
|
836
|
+
name: Server name to validate
|
|
837
|
+
|
|
838
|
+
Returns:
|
|
839
|
+
True if valid
|
|
840
|
+
"""
|
|
841
|
+
import re
|
|
842
|
+
|
|
843
|
+
if not name or len(name) < 3 or len(name) > 50:
|
|
844
|
+
return False
|
|
845
|
+
|
|
846
|
+
if name != name.lower():
|
|
847
|
+
return False
|
|
848
|
+
|
|
849
|
+
if not re.match(r'^[a-z0-9][a-z0-9-]*[a-z0-9]$', name) and len(name) > 2:
|
|
850
|
+
return False
|
|
851
|
+
|
|
852
|
+
if len(name) <= 2:
|
|
853
|
+
return re.match(r'^[a-z0-9]+$', name) is not None
|
|
854
|
+
|
|
855
|
+
return True
|
|
856
|
+
|
|
857
|
+
|
|
858
|
+
# Singleton instance
|
|
859
|
+
_user_mcp_service: Optional[UserMCPService] = None
|
|
860
|
+
|
|
861
|
+
|
|
862
|
+
def get_user_mcp_service() -> Optional[UserMCPService]:
|
|
863
|
+
"""Get the singleton UserMCPService instance (may be None)."""
|
|
864
|
+
return _user_mcp_service
|
|
865
|
+
|
|
866
|
+
|
|
867
|
+
def require_user_mcp_service() -> UserMCPService:
|
|
868
|
+
"""
|
|
869
|
+
Get the singleton UserMCPService instance.
|
|
870
|
+
|
|
871
|
+
FIX #10: Raises error if service not initialized, preventing None checks everywhere.
|
|
872
|
+
|
|
873
|
+
Raises:
|
|
874
|
+
RuntimeError: If service not initialized
|
|
875
|
+
|
|
876
|
+
Returns:
|
|
877
|
+
Initialized UserMCPService
|
|
878
|
+
"""
|
|
879
|
+
if _user_mcp_service is None:
|
|
880
|
+
raise RuntimeError(
|
|
881
|
+
"UserMCPService not initialized. Call init_user_mcp_service() first."
|
|
882
|
+
)
|
|
883
|
+
return _user_mcp_service
|
|
884
|
+
|
|
885
|
+
|
|
886
|
+
def init_user_mcp_service(db_handler: UserMCPServersHandler, **kwargs) -> UserMCPService:
|
|
887
|
+
"""
|
|
888
|
+
Initialize the singleton UserMCPService.
|
|
889
|
+
|
|
890
|
+
Args:
|
|
891
|
+
db_handler: Database handler
|
|
892
|
+
**kwargs: Additional options (encryption, url_validator, allow_http)
|
|
893
|
+
|
|
894
|
+
Returns:
|
|
895
|
+
Initialized UserMCPService
|
|
896
|
+
"""
|
|
897
|
+
global _user_mcp_service
|
|
898
|
+
_user_mcp_service = UserMCPService(db_handler, **kwargs)
|
|
899
|
+
return _user_mcp_service
|