flowent 0.0.5 → 0.0.7
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.
- package/README.md +1 -1
- package/backend/README.md +1 -1
- package/backend/pyproject.toml +1 -1
- package/backend/src/flowent/__pycache__/__init__.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/_version.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/access.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/agent.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/assistant_commands.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/cli.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/config.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/events.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/graph_runtime.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/graph_service.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/image_assets.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/logging.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/main.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/model_metadata.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/network.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/observability_service.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/registry.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/role_management.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/runtime.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/sandbox.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/security.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/settings.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/settings_management.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/state_db.cpython-313.pyc +0 -0
- package/backend/src/flowent/__pycache__/workspace_store.cpython-313.pyc +0 -0
- package/backend/src/flowent/agent.py +91 -8
- package/backend/src/flowent/channels/__pycache__/__init__.cpython-313.pyc +0 -0
- package/backend/src/flowent/channels/__pycache__/telegram.cpython-313.pyc +0 -0
- package/backend/src/flowent/graph_service.py +3 -110
- package/backend/src/flowent/models/__pycache__/__init__.cpython-313.pyc +0 -0
- package/backend/src/flowent/models/__pycache__/agent.cpython-313.pyc +0 -0
- package/backend/src/flowent/models/__pycache__/base.cpython-313.pyc +0 -0
- package/backend/src/flowent/models/__pycache__/blueprint.cpython-313.pyc +0 -0
- package/backend/src/flowent/models/__pycache__/content.cpython-313.pyc +0 -0
- package/backend/src/flowent/models/__pycache__/delta.cpython-313.pyc +0 -0
- package/backend/src/flowent/models/__pycache__/event.cpython-313.pyc +0 -0
- package/backend/src/flowent/models/__pycache__/graph.cpython-313.pyc +0 -0
- package/backend/src/flowent/models/__pycache__/history.cpython-313.pyc +0 -0
- package/backend/src/flowent/models/__pycache__/llm.cpython-313.pyc +0 -0
- package/backend/src/flowent/models/__pycache__/message.cpython-313.pyc +0 -0
- package/backend/src/flowent/models/__pycache__/tab.cpython-313.pyc +0 -0
- package/backend/src/flowent/models/__pycache__/todo.cpython-313.pyc +0 -0
- package/backend/src/flowent/prompts/__pycache__/__init__.cpython-313.pyc +0 -0
- package/backend/src/flowent/prompts/__pycache__/common.cpython-313.pyc +0 -0
- package/backend/src/flowent/prompts/__pycache__/steward.cpython-313.pyc +0 -0
- package/backend/src/flowent/providers/__pycache__/__init__.cpython-313.pyc +0 -0
- package/backend/src/flowent/providers/__pycache__/anthropic.cpython-313.pyc +0 -0
- package/backend/src/flowent/providers/__pycache__/base_url.cpython-313.pyc +0 -0
- package/backend/src/flowent/providers/__pycache__/configuration.cpython-313.pyc +0 -0
- package/backend/src/flowent/providers/__pycache__/content.cpython-313.pyc +0 -0
- package/backend/src/flowent/providers/__pycache__/errors.cpython-313.pyc +0 -0
- package/backend/src/flowent/providers/__pycache__/gateway.cpython-313.pyc +0 -0
- package/backend/src/flowent/providers/__pycache__/headers.cpython-313.pyc +0 -0
- package/backend/src/flowent/providers/__pycache__/management.cpython-313.pyc +0 -0
- package/backend/src/flowent/providers/__pycache__/openai.cpython-313.pyc +0 -0
- package/backend/src/flowent/providers/__pycache__/openai_responses.cpython-313.pyc +0 -0
- package/backend/src/flowent/providers/__pycache__/registry.cpython-313.pyc +0 -0
- package/backend/src/flowent/providers/__pycache__/sse.cpython-313.pyc +0 -0
- package/backend/src/flowent/providers/__pycache__/thinking.cpython-313.pyc +0 -0
- package/backend/src/flowent/role_management.py +9 -6
- package/backend/src/flowent/routes/__init__.py +0 -2
- package/backend/src/flowent/routes/__pycache__/__init__.cpython-313.pyc +0 -0
- package/backend/src/flowent/routes/__pycache__/access.cpython-313.pyc +0 -0
- package/backend/src/flowent/routes/__pycache__/assistant.cpython-313.pyc +0 -0
- package/backend/src/flowent/routes/__pycache__/image_assets.cpython-313.pyc +0 -0
- package/backend/src/flowent/routes/__pycache__/meta.cpython-313.pyc +0 -0
- package/backend/src/flowent/routes/__pycache__/nodes.cpython-313.pyc +0 -0
- package/backend/src/flowent/routes/__pycache__/prompts.cpython-313.pyc +0 -0
- package/backend/src/flowent/routes/__pycache__/providers_route.cpython-313.pyc +0 -0
- package/backend/src/flowent/routes/__pycache__/roles.cpython-313.pyc +0 -0
- package/backend/src/flowent/routes/__pycache__/settings.cpython-313.pyc +0 -0
- package/backend/src/flowent/routes/__pycache__/tabs.cpython-313.pyc +0 -0
- package/backend/src/flowent/routes/__pycache__/ws.cpython-313.pyc +0 -0
- package/backend/src/flowent/routes/assistant.py +3 -0
- package/backend/src/flowent/routes/nodes.py +11 -1
- package/backend/src/flowent/routes/settings.py +169 -118
- package/backend/src/flowent/routes/tabs.py +0 -12
- package/backend/src/flowent/runtime.py +0 -5
- package/backend/src/flowent/security.py +1 -21
- package/backend/src/flowent/settings.py +15 -421
- package/backend/src/flowent/settings_management.py +260 -164
- package/backend/src/flowent/state_db.py +2 -14
- package/backend/src/flowent/static/assets/AssistantPage-BW7XAd9I.js +1 -0
- package/backend/src/flowent/static/assets/ChannelsPage-tCJHgt6m.js +1 -0
- package/backend/src/flowent/static/assets/{PageScaffold-DteOA8V7.js → PageScaffold-f6g2l7XN.js} +1 -1
- package/backend/src/flowent/static/assets/PromptsPage-C3Sxn2D7.js +1 -0
- package/backend/src/flowent/static/assets/ProvidersPage-BfmdXmNt.js +3 -0
- package/backend/src/flowent/static/assets/RolesPage-DET8wO4r.js +1 -0
- package/backend/src/flowent/static/assets/SettingsPage-D-g3deMm.js +3 -0
- package/backend/src/flowent/static/assets/ToolsPage-CDmtE2g4.js +1 -0
- package/backend/src/flowent/static/assets/WorkspacePage-AZsJ0sD0.js +3 -0
- package/backend/src/flowent/static/assets/WorkspacePanels-CteCjolX.js +1 -0
- package/backend/src/flowent/static/assets/{alert-dialog-DIBUCmqM.js → alert-dialog-Duorp_S-.js} +1 -1
- package/backend/src/flowent/static/assets/{dialog-BOvHIBrg.js → dialog-C3ixjGjN.js} +1 -1
- package/backend/src/flowent/static/assets/index--o_0fv0N.css +1 -0
- package/backend/src/flowent/static/assets/index-C9HuekJm.js +10 -0
- package/backend/src/flowent/static/assets/{modelParams-DcEhGnu0.js → modelParams-DmnF2hwR.js} +1 -1
- package/backend/src/flowent/static/assets/providerTypes-DT3Ahwl_.js +1 -0
- package/backend/src/flowent/static/assets/roles-CuRT_chR.js +1 -0
- package/{dist/frontend/assets/select-D9SwnlXF.js → backend/src/flowent/static/assets/select-DCfeNu-F.js} +1 -1
- package/backend/src/flowent/static/assets/surface-pWwG5ogx.js +1 -0
- package/backend/src/flowent/static/assets/{ui-vendor-UazN8rcv.js → ui-vendor-C5pJa8N7.js} +15 -15
- package/backend/src/flowent/static/assets/useAppRoute-FgSHBKhV.js +1 -0
- package/backend/src/flowent/static/index.html +3 -3
- package/backend/src/flowent/tools/__init__.py +2 -101
- package/backend/src/flowent/tools/__pycache__/__init__.cpython-313.pyc +0 -0
- package/backend/src/flowent/tools/__pycache__/connect.cpython-313.pyc +0 -0
- package/backend/src/flowent/tools/__pycache__/contacts.cpython-313.pyc +0 -0
- package/backend/src/flowent/tools/__pycache__/create_agent.cpython-313.pyc +0 -0
- package/backend/src/flowent/tools/__pycache__/create_tab.cpython-313.pyc +0 -0
- package/backend/src/flowent/tools/__pycache__/delete_tab.cpython-313.pyc +0 -0
- package/backend/src/flowent/tools/__pycache__/edit.cpython-313.pyc +0 -0
- package/backend/src/flowent/tools/__pycache__/exec.cpython-313.pyc +0 -0
- package/backend/src/flowent/tools/__pycache__/fetch.cpython-313.pyc +0 -0
- package/backend/src/flowent/tools/__pycache__/idle.cpython-313.pyc +0 -0
- package/backend/src/flowent/tools/__pycache__/list_roles.cpython-313.pyc +0 -0
- package/backend/src/flowent/tools/__pycache__/list_tabs.cpython-313.pyc +0 -0
- package/backend/src/flowent/tools/__pycache__/list_tools.cpython-313.pyc +0 -0
- package/backend/src/flowent/tools/__pycache__/manage_prompts.cpython-313.pyc +0 -0
- package/backend/src/flowent/tools/__pycache__/manage_providers.cpython-313.pyc +0 -0
- package/backend/src/flowent/tools/__pycache__/manage_roles.cpython-313.pyc +0 -0
- package/backend/src/flowent/tools/__pycache__/manage_settings.cpython-313.pyc +0 -0
- package/backend/src/flowent/tools/__pycache__/read.cpython-313.pyc +0 -0
- package/backend/src/flowent/tools/__pycache__/send.cpython-313.pyc +0 -0
- package/backend/src/flowent/tools/__pycache__/set_permissions.cpython-313.pyc +0 -0
- package/backend/src/flowent/tools/__pycache__/sleep.cpython-313.pyc +0 -0
- package/backend/src/flowent/tools/__pycache__/todo.cpython-313.pyc +0 -0
- package/backend/src/flowent/tools/list_roles.py +2 -9
- package/backend/src/flowent/tools/manage_settings.py +134 -172
- package/backend/tests/__pycache__/__init__.cpython-313.pyc +0 -0
- package/backend/tests/__pycache__/conftest.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/integration/api/__pycache__/conftest.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/integration/api/__pycache__/test_access_api.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/integration/api/__pycache__/test_assistant_api.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/integration/api/__pycache__/test_frontend_mounting.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/integration/api/__pycache__/test_meta_api.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/integration/api/__pycache__/test_nodes_api.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/integration/api/__pycache__/test_prompts_api.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/integration/api/__pycache__/test_roles_api.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/integration/api/__pycache__/test_tabs_api.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/integration/api/test_assistant_api.py +68 -0
- package/backend/tests/integration/api/test_meta_api.py +0 -1
- package/backend/tests/integration/api/test_nodes_api.py +73 -8
- package/backend/tests/integration/api/test_tabs_api.py +0 -114
- package/backend/tests/unit/__pycache__/test_access.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/unit/__pycache__/test_cli.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/unit/__pycache__/test_graph_runtime.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/unit/__pycache__/test_network.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/unit/__pycache__/test_state_sqlite_storage.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/unit/__pycache__/test_workspace_store.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/unit/agent/__pycache__/test_agent_public_api.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/unit/agent/__pycache__/test_agent_runtime.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/unit/agent/test_agent_public_api.py +0 -15
- package/backend/tests/unit/agent/test_agent_runtime.py +148 -2
- package/backend/tests/unit/channels/__pycache__/test_telegram_channel.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/unit/logging/__pycache__/test_logging.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/unit/prompts/__pycache__/test_prompts.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/unit/providers/__pycache__/test_anthropic_provider.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/unit/providers/__pycache__/test_errors.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/unit/providers/__pycache__/test_extract_delta_parts.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/unit/providers/__pycache__/test_openai_provider.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/unit/providers/__pycache__/test_openai_responses.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/unit/providers/__pycache__/test_provider_gateway.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/unit/providers/__pycache__/test_think_tag_parser.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/unit/routes/__pycache__/test_prompts_routes.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/unit/routes/__pycache__/test_providers_route.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/unit/routes/__pycache__/test_roles_routes.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/unit/routes/__pycache__/test_settings_routes.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/unit/routes/test_prompts_routes.py +0 -22
- package/backend/tests/unit/routes/test_roles_routes.py +6 -2
- package/backend/tests/unit/routes/test_settings_routes.py +0 -19
- package/backend/tests/unit/runtime/__pycache__/test_bootstrap_runtime.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/unit/sandbox/__pycache__/test_sandbox_tools.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/unit/security/__pycache__/test_security.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/unit/settings/__pycache__/test_settings_roles.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/unit/settings/test_settings_roles.py +3 -51
- package/backend/tests/unit/test_cli.py +0 -22
- package/backend/tests/unit/test_state_sqlite_storage.py +27 -99
- package/backend/tests/unit/test_workspace_store.py +0 -3
- package/backend/tests/unit/tools/__pycache__/test_connect_tool.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/unit/tools/__pycache__/test_create_agent_tool.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/unit/tools/__pycache__/test_delete_tab_tool.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/unit/tools/__pycache__/test_edit_tool.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/unit/tools/__pycache__/test_exec_tool.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/unit/tools/__pycache__/test_fetch_tool.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/unit/tools/__pycache__/test_manage_prompts_tool.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/unit/tools/__pycache__/test_manage_providers_tool.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/unit/tools/__pycache__/test_manage_roles_tool.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/unit/tools/__pycache__/test_manage_settings_tool.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/unit/tools/__pycache__/test_read_tool.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/unit/tools/__pycache__/test_set_permissions_tool.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/unit/tools/__pycache__/test_todo_tool.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/unit/tools/__pycache__/test_tool_registry.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/unit/tools/test_create_agent_tool.py +0 -32
- package/backend/tests/unit/tools/test_manage_prompts_tool.py +5 -30
- package/backend/tests/unit/tools/test_tool_registry.py +45 -40
- package/backend/uv.lock +1 -1
- package/dist/frontend/assets/AssistantPage-BW7XAd9I.js +1 -0
- package/dist/frontend/assets/ChannelsPage-tCJHgt6m.js +1 -0
- package/dist/frontend/assets/{PageScaffold-DteOA8V7.js → PageScaffold-f6g2l7XN.js} +1 -1
- package/dist/frontend/assets/PromptsPage-C3Sxn2D7.js +1 -0
- package/dist/frontend/assets/ProvidersPage-BfmdXmNt.js +3 -0
- package/dist/frontend/assets/RolesPage-DET8wO4r.js +1 -0
- package/dist/frontend/assets/SettingsPage-D-g3deMm.js +3 -0
- package/dist/frontend/assets/ToolsPage-CDmtE2g4.js +1 -0
- package/dist/frontend/assets/WorkspacePage-AZsJ0sD0.js +3 -0
- package/dist/frontend/assets/WorkspacePanels-CteCjolX.js +1 -0
- package/dist/frontend/assets/{alert-dialog-DIBUCmqM.js → alert-dialog-Duorp_S-.js} +1 -1
- package/dist/frontend/assets/{dialog-BOvHIBrg.js → dialog-C3ixjGjN.js} +1 -1
- package/dist/frontend/assets/index--o_0fv0N.css +1 -0
- package/dist/frontend/assets/index-C9HuekJm.js +10 -0
- package/dist/frontend/assets/{modelParams-DcEhGnu0.js → modelParams-DmnF2hwR.js} +1 -1
- package/dist/frontend/assets/providerTypes-DT3Ahwl_.js +1 -0
- package/dist/frontend/assets/roles-CuRT_chR.js +1 -0
- package/{backend/src/flowent/static/assets/select-D9SwnlXF.js → dist/frontend/assets/select-DCfeNu-F.js} +1 -1
- package/dist/frontend/assets/surface-pWwG5ogx.js +1 -0
- package/dist/frontend/assets/{ui-vendor-UazN8rcv.js → ui-vendor-C5pJa8N7.js} +15 -15
- package/dist/frontend/assets/useAppRoute-FgSHBKhV.js +1 -0
- package/dist/frontend/index.html +3 -3
- package/package.json +1 -1
- package/backend/src/flowent/__pycache__/mcp_service.cpython-313.pyc +0 -0
- package/backend/src/flowent/mcp_service.py +0 -1918
- package/backend/src/flowent/routes/__pycache__/mcp.cpython-313.pyc +0 -0
- package/backend/src/flowent/routes/mcp.py +0 -125
- package/backend/src/flowent/static/assets/AssistantPage-VBohhz4d.js +0 -1
- package/backend/src/flowent/static/assets/ChannelsPage-CIydPZA_.js +0 -1
- package/backend/src/flowent/static/assets/McpPage-CHPm2TPY.js +0 -7
- package/backend/src/flowent/static/assets/PromptsPage-CSmJ3sZg.js +0 -1
- package/backend/src/flowent/static/assets/ProvidersPage-sl2jeG4e.js +0 -3
- package/backend/src/flowent/static/assets/RolesPage-DCe7W6Km.js +0 -1
- package/backend/src/flowent/static/assets/SettingsPage-Bix9e63E.js +0 -3
- package/backend/src/flowent/static/assets/ToolsPage-favNkj5C.js +0 -1
- package/backend/src/flowent/static/assets/WorkspaceCommandDialog-DRS6wiD6.js +0 -1
- package/backend/src/flowent/static/assets/WorkspacePage-KuaDjt_D.js +0 -3
- package/backend/src/flowent/static/assets/WorkspacePanels-BZxBw8M5.js +0 -1
- package/backend/src/flowent/static/assets/datetime-eJqd0V2S.js +0 -1
- package/backend/src/flowent/static/assets/index-Biio-CoI.js +0 -10
- package/backend/src/flowent/static/assets/index-CmQvO7sl.css +0 -1
- package/backend/src/flowent/static/assets/roles-BbIEIMeG.js +0 -1
- package/backend/src/flowent/static/assets/surface-Bzr1FRG4.js +0 -1
- package/backend/src/flowent/static/assets/triState-DgLlKdRR.js +0 -1
- package/backend/src/flowent/tools/__pycache__/mcp.cpython-313.pyc +0 -0
- package/backend/src/flowent/tools/mcp.py +0 -199
- package/backend/tests/integration/api/__pycache__/test_mcp_api.cpython-313-pytest-9.0.3.pyc +0 -0
- package/backend/tests/integration/api/test_mcp_api.py +0 -116
- package/dist/frontend/assets/AssistantPage-VBohhz4d.js +0 -1
- package/dist/frontend/assets/ChannelsPage-CIydPZA_.js +0 -1
- package/dist/frontend/assets/McpPage-CHPm2TPY.js +0 -7
- package/dist/frontend/assets/PromptsPage-CSmJ3sZg.js +0 -1
- package/dist/frontend/assets/ProvidersPage-sl2jeG4e.js +0 -3
- package/dist/frontend/assets/RolesPage-DCe7W6Km.js +0 -1
- package/dist/frontend/assets/SettingsPage-Bix9e63E.js +0 -3
- package/dist/frontend/assets/ToolsPage-favNkj5C.js +0 -1
- package/dist/frontend/assets/WorkspaceCommandDialog-DRS6wiD6.js +0 -1
- package/dist/frontend/assets/WorkspacePage-KuaDjt_D.js +0 -3
- package/dist/frontend/assets/WorkspacePanels-BZxBw8M5.js +0 -1
- package/dist/frontend/assets/datetime-eJqd0V2S.js +0 -1
- package/dist/frontend/assets/index-Biio-CoI.js +0 -10
- package/dist/frontend/assets/index-CmQvO7sl.css +0 -1
- package/dist/frontend/assets/roles-BbIEIMeG.js +0 -1
- package/dist/frontend/assets/surface-Bzr1FRG4.js +0 -1
- package/dist/frontend/assets/triState-DgLlKdRR.js +0 -1
|
@@ -1,1918 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import json
|
|
4
|
-
import os
|
|
5
|
-
import queue
|
|
6
|
-
import subprocess
|
|
7
|
-
import threading
|
|
8
|
-
import time
|
|
9
|
-
import uuid
|
|
10
|
-
from collections import deque
|
|
11
|
-
from contextlib import suppress
|
|
12
|
-
from dataclasses import dataclass, field
|
|
13
|
-
from pathlib import Path
|
|
14
|
-
from typing import TYPE_CHECKING, Any
|
|
15
|
-
|
|
16
|
-
from curl_cffi import requests as curl_requests
|
|
17
|
-
|
|
18
|
-
from flowent.settings import (
|
|
19
|
-
MCPServerConfig,
|
|
20
|
-
find_mcp_server,
|
|
21
|
-
get_settings,
|
|
22
|
-
save_settings,
|
|
23
|
-
)
|
|
24
|
-
from flowent.state_db import open_state_db
|
|
25
|
-
|
|
26
|
-
if TYPE_CHECKING:
|
|
27
|
-
from flowent.agent import Agent
|
|
28
|
-
|
|
29
|
-
PROTOCOL_VERSION = "2025-06-18"
|
|
30
|
-
ACTIVITY_RETENTION_SECONDS = 30 * 24 * 60 * 60
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
class MCPError(RuntimeError):
|
|
34
|
-
pass
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
@dataclass
|
|
38
|
-
class MCPToolDescriptor:
|
|
39
|
-
server_name: str
|
|
40
|
-
tool_name: str
|
|
41
|
-
fully_qualified_id: str
|
|
42
|
-
title: str | None = None
|
|
43
|
-
description: str = ""
|
|
44
|
-
input_schema: dict[str, Any] = field(default_factory=dict)
|
|
45
|
-
read_only_hint: bool = False
|
|
46
|
-
destructive_hint: bool = False
|
|
47
|
-
open_world_hint: bool = False
|
|
48
|
-
|
|
49
|
-
def serialize(self) -> dict[str, object]:
|
|
50
|
-
return {
|
|
51
|
-
"name": self.fully_qualified_id,
|
|
52
|
-
"source": "mcp",
|
|
53
|
-
"server_name": self.server_name,
|
|
54
|
-
"tool_name": self.tool_name,
|
|
55
|
-
"fully_qualified_id": self.fully_qualified_id,
|
|
56
|
-
"title": self.title,
|
|
57
|
-
"description": self.description,
|
|
58
|
-
"parameters": self.input_schema,
|
|
59
|
-
"read_only_hint": self.read_only_hint,
|
|
60
|
-
"destructive_hint": self.destructive_hint,
|
|
61
|
-
"open_world_hint": self.open_world_hint,
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
@dataclass
|
|
66
|
-
class MCPResourceDescriptor:
|
|
67
|
-
server_name: str
|
|
68
|
-
name: str
|
|
69
|
-
uri: str
|
|
70
|
-
mime_type: str | None = None
|
|
71
|
-
description: str | None = None
|
|
72
|
-
|
|
73
|
-
def serialize(self) -> dict[str, object]:
|
|
74
|
-
return {
|
|
75
|
-
"server_name": self.server_name,
|
|
76
|
-
"name": self.name,
|
|
77
|
-
"uri": self.uri,
|
|
78
|
-
"mime_type": self.mime_type,
|
|
79
|
-
"description": self.description,
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
@dataclass
|
|
84
|
-
class MCPResourceTemplateDescriptor:
|
|
85
|
-
server_name: str
|
|
86
|
-
name: str
|
|
87
|
-
uri_template: str
|
|
88
|
-
description: str | None = None
|
|
89
|
-
|
|
90
|
-
def serialize(self) -> dict[str, object]:
|
|
91
|
-
return {
|
|
92
|
-
"server_name": self.server_name,
|
|
93
|
-
"name": self.name,
|
|
94
|
-
"uri_template": self.uri_template,
|
|
95
|
-
"description": self.description,
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
@dataclass
|
|
100
|
-
class MCPPromptDescriptor:
|
|
101
|
-
server_name: str
|
|
102
|
-
name: str
|
|
103
|
-
description: str | None = None
|
|
104
|
-
arguments: list[dict[str, object]] = field(default_factory=list)
|
|
105
|
-
|
|
106
|
-
def serialize(self) -> dict[str, object]:
|
|
107
|
-
return {
|
|
108
|
-
"server_name": self.server_name,
|
|
109
|
-
"name": self.name,
|
|
110
|
-
"description": self.description,
|
|
111
|
-
"arguments": list(self.arguments),
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
@dataclass
|
|
116
|
-
class MCPDiscoverySnapshot:
|
|
117
|
-
server_name: str
|
|
118
|
-
transport: str
|
|
119
|
-
status: str
|
|
120
|
-
auth_status: str
|
|
121
|
-
last_auth_result: str | None = None
|
|
122
|
-
last_refresh_at: float | None = None
|
|
123
|
-
last_refresh_result: str = "never"
|
|
124
|
-
last_error: str | None = None
|
|
125
|
-
tools: list[MCPToolDescriptor] = field(default_factory=list)
|
|
126
|
-
resources: list[MCPResourceDescriptor] = field(default_factory=list)
|
|
127
|
-
resource_templates: list[MCPResourceTemplateDescriptor] = field(
|
|
128
|
-
default_factory=list
|
|
129
|
-
)
|
|
130
|
-
prompts: list[MCPPromptDescriptor] = field(default_factory=list)
|
|
131
|
-
|
|
132
|
-
def serialize(self) -> dict[str, object]:
|
|
133
|
-
return {
|
|
134
|
-
"server_name": self.server_name,
|
|
135
|
-
"transport": self.transport,
|
|
136
|
-
"status": self.status,
|
|
137
|
-
"auth_status": self.auth_status,
|
|
138
|
-
"last_auth_result": self.last_auth_result,
|
|
139
|
-
"last_refresh_at": self.last_refresh_at,
|
|
140
|
-
"last_refresh_result": self.last_refresh_result,
|
|
141
|
-
"last_error": self.last_error,
|
|
142
|
-
"tools": [item.serialize() for item in self.tools],
|
|
143
|
-
"resources": [item.serialize() for item in self.resources],
|
|
144
|
-
"resource_templates": [
|
|
145
|
-
item.serialize() for item in self.resource_templates
|
|
146
|
-
],
|
|
147
|
-
"prompts": [item.serialize() for item in self.prompts],
|
|
148
|
-
"capability_counts": {
|
|
149
|
-
"tools": len(self.tools),
|
|
150
|
-
"resources": len(self.resources),
|
|
151
|
-
"resource_templates": len(self.resource_templates),
|
|
152
|
-
"prompts": len(self.prompts),
|
|
153
|
-
},
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
@dataclass
|
|
158
|
-
class MCPActivityRecord:
|
|
159
|
-
id: str
|
|
160
|
-
server_name: str
|
|
161
|
-
action: str
|
|
162
|
-
actor_node_id: str | None
|
|
163
|
-
tab_id: str | None
|
|
164
|
-
started_at: float
|
|
165
|
-
ended_at: float
|
|
166
|
-
result: str
|
|
167
|
-
summary: str
|
|
168
|
-
tool_name: str | None = None
|
|
169
|
-
fully_qualified_id: str | None = None
|
|
170
|
-
target: str | None = None
|
|
171
|
-
approval_result: str | None = None
|
|
172
|
-
|
|
173
|
-
def serialize(self) -> dict[str, object]:
|
|
174
|
-
return {
|
|
175
|
-
"id": self.id,
|
|
176
|
-
"server_name": self.server_name,
|
|
177
|
-
"action": self.action,
|
|
178
|
-
"actor_node_id": self.actor_node_id,
|
|
179
|
-
"tab_id": self.tab_id,
|
|
180
|
-
"started_at": self.started_at,
|
|
181
|
-
"ended_at": self.ended_at,
|
|
182
|
-
"duration_ms": max(0.0, (self.ended_at - self.started_at) * 1000),
|
|
183
|
-
"result": self.result,
|
|
184
|
-
"summary": self.summary,
|
|
185
|
-
"tool_name": self.tool_name,
|
|
186
|
-
"fully_qualified_id": self.fully_qualified_id,
|
|
187
|
-
"target": self.target,
|
|
188
|
-
"approval_result": self.approval_result,
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
def _deserialize_tool_descriptor(data: dict[str, object]) -> MCPToolDescriptor | None:
|
|
193
|
-
server_name = data.get("server_name")
|
|
194
|
-
tool_name = data.get("tool_name")
|
|
195
|
-
fully_qualified_id = data.get("fully_qualified_id")
|
|
196
|
-
if (
|
|
197
|
-
not isinstance(server_name, str)
|
|
198
|
-
or not isinstance(tool_name, str)
|
|
199
|
-
or not isinstance(fully_qualified_id, str)
|
|
200
|
-
):
|
|
201
|
-
return None
|
|
202
|
-
title = data.get("title")
|
|
203
|
-
description = data.get("description")
|
|
204
|
-
parameters = data.get("parameters")
|
|
205
|
-
return MCPToolDescriptor(
|
|
206
|
-
server_name=server_name,
|
|
207
|
-
tool_name=tool_name,
|
|
208
|
-
fully_qualified_id=fully_qualified_id,
|
|
209
|
-
title=title if isinstance(title, str) else None,
|
|
210
|
-
description=description if isinstance(description, str) else "",
|
|
211
|
-
input_schema=parameters if isinstance(parameters, dict) else {},
|
|
212
|
-
read_only_hint=bool(data.get("read_only_hint", False)),
|
|
213
|
-
destructive_hint=bool(data.get("destructive_hint", False)),
|
|
214
|
-
open_world_hint=bool(data.get("open_world_hint", False)),
|
|
215
|
-
)
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
def _deserialize_resource_descriptor(
|
|
219
|
-
data: dict[str, object],
|
|
220
|
-
) -> MCPResourceDescriptor | None:
|
|
221
|
-
server_name = data.get("server_name")
|
|
222
|
-
name = data.get("name")
|
|
223
|
-
uri = data.get("uri")
|
|
224
|
-
if (
|
|
225
|
-
not isinstance(server_name, str)
|
|
226
|
-
or not isinstance(name, str)
|
|
227
|
-
or not isinstance(uri, str)
|
|
228
|
-
):
|
|
229
|
-
return None
|
|
230
|
-
mime_type = data.get("mime_type")
|
|
231
|
-
description = data.get("description")
|
|
232
|
-
return MCPResourceDescriptor(
|
|
233
|
-
server_name=server_name,
|
|
234
|
-
name=name,
|
|
235
|
-
uri=uri,
|
|
236
|
-
mime_type=mime_type if isinstance(mime_type, str) else None,
|
|
237
|
-
description=description if isinstance(description, str) else None,
|
|
238
|
-
)
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
def _deserialize_resource_template_descriptor(
|
|
242
|
-
data: dict[str, object],
|
|
243
|
-
) -> MCPResourceTemplateDescriptor | None:
|
|
244
|
-
server_name = data.get("server_name")
|
|
245
|
-
name = data.get("name")
|
|
246
|
-
uri_template = data.get("uri_template")
|
|
247
|
-
if (
|
|
248
|
-
not isinstance(server_name, str)
|
|
249
|
-
or not isinstance(name, str)
|
|
250
|
-
or not isinstance(uri_template, str)
|
|
251
|
-
):
|
|
252
|
-
return None
|
|
253
|
-
description = data.get("description")
|
|
254
|
-
return MCPResourceTemplateDescriptor(
|
|
255
|
-
server_name=server_name,
|
|
256
|
-
name=name,
|
|
257
|
-
uri_template=uri_template,
|
|
258
|
-
description=description if isinstance(description, str) else None,
|
|
259
|
-
)
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
def _deserialize_prompt_descriptor(
|
|
263
|
-
data: dict[str, object],
|
|
264
|
-
) -> MCPPromptDescriptor | None:
|
|
265
|
-
server_name = data.get("server_name")
|
|
266
|
-
name = data.get("name")
|
|
267
|
-
if not isinstance(server_name, str) or not isinstance(name, str):
|
|
268
|
-
return None
|
|
269
|
-
description = data.get("description")
|
|
270
|
-
arguments = data.get("arguments")
|
|
271
|
-
return MCPPromptDescriptor(
|
|
272
|
-
server_name=server_name,
|
|
273
|
-
name=name,
|
|
274
|
-
description=description if isinstance(description, str) else None,
|
|
275
|
-
arguments=[
|
|
276
|
-
dict(argument) for argument in arguments if isinstance(argument, dict)
|
|
277
|
-
]
|
|
278
|
-
if isinstance(arguments, list)
|
|
279
|
-
else [],
|
|
280
|
-
)
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
def _snapshot_from_mapping(data: dict[str, object]) -> MCPDiscoverySnapshot | None:
|
|
284
|
-
server_name = data.get("server_name")
|
|
285
|
-
transport = data.get("transport")
|
|
286
|
-
status = data.get("status")
|
|
287
|
-
auth_status = data.get("auth_status")
|
|
288
|
-
if (
|
|
289
|
-
not isinstance(server_name, str)
|
|
290
|
-
or not isinstance(transport, str)
|
|
291
|
-
or not isinstance(status, str)
|
|
292
|
-
or not isinstance(auth_status, str)
|
|
293
|
-
):
|
|
294
|
-
return None
|
|
295
|
-
raw_tools = data.get("tools")
|
|
296
|
-
raw_resources = data.get("resources")
|
|
297
|
-
raw_resource_templates = data.get("resource_templates")
|
|
298
|
-
raw_prompts = data.get("prompts")
|
|
299
|
-
raw_last_auth_result = data.get("last_auth_result")
|
|
300
|
-
raw_last_refresh_at = data.get("last_refresh_at")
|
|
301
|
-
raw_last_refresh_result = data.get("last_refresh_result")
|
|
302
|
-
raw_last_error = data.get("last_error")
|
|
303
|
-
tools = (
|
|
304
|
-
[
|
|
305
|
-
descriptor
|
|
306
|
-
for descriptor in (
|
|
307
|
-
_deserialize_tool_descriptor(item)
|
|
308
|
-
for item in raw_tools
|
|
309
|
-
if isinstance(item, dict)
|
|
310
|
-
)
|
|
311
|
-
if descriptor is not None
|
|
312
|
-
]
|
|
313
|
-
if isinstance(raw_tools, list)
|
|
314
|
-
else []
|
|
315
|
-
)
|
|
316
|
-
resources = (
|
|
317
|
-
[
|
|
318
|
-
descriptor
|
|
319
|
-
for descriptor in (
|
|
320
|
-
_deserialize_resource_descriptor(item)
|
|
321
|
-
for item in raw_resources
|
|
322
|
-
if isinstance(item, dict)
|
|
323
|
-
)
|
|
324
|
-
if descriptor is not None
|
|
325
|
-
]
|
|
326
|
-
if isinstance(raw_resources, list)
|
|
327
|
-
else []
|
|
328
|
-
)
|
|
329
|
-
resource_templates = (
|
|
330
|
-
[
|
|
331
|
-
descriptor
|
|
332
|
-
for descriptor in (
|
|
333
|
-
_deserialize_resource_template_descriptor(item)
|
|
334
|
-
for item in raw_resource_templates
|
|
335
|
-
if isinstance(item, dict)
|
|
336
|
-
)
|
|
337
|
-
if descriptor is not None
|
|
338
|
-
]
|
|
339
|
-
if isinstance(raw_resource_templates, list)
|
|
340
|
-
else []
|
|
341
|
-
)
|
|
342
|
-
prompts = (
|
|
343
|
-
[
|
|
344
|
-
descriptor
|
|
345
|
-
for descriptor in (
|
|
346
|
-
_deserialize_prompt_descriptor(item)
|
|
347
|
-
for item in raw_prompts
|
|
348
|
-
if isinstance(item, dict)
|
|
349
|
-
)
|
|
350
|
-
if descriptor is not None
|
|
351
|
-
]
|
|
352
|
-
if isinstance(raw_prompts, list)
|
|
353
|
-
else []
|
|
354
|
-
)
|
|
355
|
-
return MCPDiscoverySnapshot(
|
|
356
|
-
server_name=server_name,
|
|
357
|
-
transport=transport,
|
|
358
|
-
status=status,
|
|
359
|
-
auth_status=auth_status,
|
|
360
|
-
last_auth_result=(
|
|
361
|
-
raw_last_auth_result if isinstance(raw_last_auth_result, str) else None
|
|
362
|
-
),
|
|
363
|
-
last_refresh_at=(
|
|
364
|
-
float(raw_last_refresh_at)
|
|
365
|
-
if isinstance(raw_last_refresh_at, (int, float))
|
|
366
|
-
else None
|
|
367
|
-
),
|
|
368
|
-
last_refresh_result=(
|
|
369
|
-
raw_last_refresh_result
|
|
370
|
-
if isinstance(raw_last_refresh_result, str)
|
|
371
|
-
else "never"
|
|
372
|
-
),
|
|
373
|
-
last_error=raw_last_error if isinstance(raw_last_error, str) else None,
|
|
374
|
-
tools=tools,
|
|
375
|
-
resources=resources,
|
|
376
|
-
resource_templates=resource_templates,
|
|
377
|
-
prompts=prompts,
|
|
378
|
-
)
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
def _activity_from_mapping(data: dict[str, object]) -> MCPActivityRecord | None:
|
|
382
|
-
record_id = data.get("id")
|
|
383
|
-
server_name = data.get("server_name")
|
|
384
|
-
action = data.get("action")
|
|
385
|
-
started_at = data.get("started_at")
|
|
386
|
-
ended_at = data.get("ended_at")
|
|
387
|
-
result = data.get("result")
|
|
388
|
-
summary = data.get("summary")
|
|
389
|
-
if (
|
|
390
|
-
not isinstance(record_id, str)
|
|
391
|
-
or not isinstance(server_name, str)
|
|
392
|
-
or not isinstance(action, str)
|
|
393
|
-
or not isinstance(started_at, (int, float))
|
|
394
|
-
or not isinstance(ended_at, (int, float))
|
|
395
|
-
or not isinstance(result, str)
|
|
396
|
-
or not isinstance(summary, str)
|
|
397
|
-
):
|
|
398
|
-
return None
|
|
399
|
-
actor_node_id = data.get("actor_node_id")
|
|
400
|
-
tab_id = data.get("tab_id")
|
|
401
|
-
tool_name = data.get("tool_name")
|
|
402
|
-
fully_qualified_id = data.get("fully_qualified_id")
|
|
403
|
-
target = data.get("target")
|
|
404
|
-
approval_result = data.get("approval_result")
|
|
405
|
-
return MCPActivityRecord(
|
|
406
|
-
id=record_id,
|
|
407
|
-
server_name=server_name,
|
|
408
|
-
action=action,
|
|
409
|
-
actor_node_id=actor_node_id if isinstance(actor_node_id, str) else None,
|
|
410
|
-
tab_id=tab_id if isinstance(tab_id, str) else None,
|
|
411
|
-
started_at=float(started_at),
|
|
412
|
-
ended_at=float(ended_at),
|
|
413
|
-
result=result,
|
|
414
|
-
summary=summary,
|
|
415
|
-
tool_name=tool_name if isinstance(tool_name, str) else None,
|
|
416
|
-
fully_qualified_id=(
|
|
417
|
-
fully_qualified_id if isinstance(fully_qualified_id, str) else None
|
|
418
|
-
),
|
|
419
|
-
target=target if isinstance(target, str) else None,
|
|
420
|
-
approval_result=approval_result if isinstance(approval_result, str) else None,
|
|
421
|
-
)
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
def _escape_identifier(value: str) -> str:
|
|
425
|
-
parts: list[str] = []
|
|
426
|
-
for character in value:
|
|
427
|
-
if character.isalnum():
|
|
428
|
-
parts.append(character.lower())
|
|
429
|
-
continue
|
|
430
|
-
parts.append(f"_{ord(character):02x}_")
|
|
431
|
-
return "".join(parts) or "unnamed"
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
def build_fully_qualified_tool_id(server_name: str, tool_name: str) -> str:
|
|
435
|
-
return (
|
|
436
|
-
"mcp__" + _escape_identifier(server_name) + "__" + _escape_identifier(tool_name)
|
|
437
|
-
)
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
def _build_root_uri(path: str) -> str:
|
|
441
|
-
from flowent.settings import resolve_path
|
|
442
|
-
|
|
443
|
-
return resolve_path(path).as_uri()
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
def _build_roots_for_agent(agent: Agent) -> list[dict[str, str]]:
|
|
447
|
-
from flowent.graph_service import resolve_effective_permissions_for_agent
|
|
448
|
-
from flowent.settings import get_runtime_working_dir_path, resolve_path
|
|
449
|
-
|
|
450
|
-
workspace_root = str(get_runtime_working_dir_path())
|
|
451
|
-
_, boundary_dirs = resolve_effective_permissions_for_agent(agent)
|
|
452
|
-
ordered_paths: list[str] = []
|
|
453
|
-
seen: set[str] = set()
|
|
454
|
-
for path in [workspace_root, *boundary_dirs]:
|
|
455
|
-
resolved = str(resolve_path(path))
|
|
456
|
-
if resolved in seen:
|
|
457
|
-
continue
|
|
458
|
-
seen.add(resolved)
|
|
459
|
-
ordered_paths.append(resolved)
|
|
460
|
-
return [
|
|
461
|
-
{
|
|
462
|
-
"name": Path(path).name or path,
|
|
463
|
-
"uri": _build_root_uri(path),
|
|
464
|
-
}
|
|
465
|
-
for path in ordered_paths
|
|
466
|
-
]
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
def _build_stdio_env(server: MCPServerConfig) -> dict[str, str]:
|
|
470
|
-
env = dict(os.environ)
|
|
471
|
-
env.update(server.env)
|
|
472
|
-
for env_var_name in server.env_vars:
|
|
473
|
-
value = os.environ.get(env_var_name)
|
|
474
|
-
if value is not None:
|
|
475
|
-
env[env_var_name] = value
|
|
476
|
-
return env
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
def _build_http_headers(server: MCPServerConfig) -> tuple[dict[str, str], str | None]:
|
|
480
|
-
headers = dict(server.http_headers)
|
|
481
|
-
bearer_token = None
|
|
482
|
-
if server.bearer_token_env_var:
|
|
483
|
-
bearer_token = os.environ.get(server.bearer_token_env_var)
|
|
484
|
-
if bearer_token:
|
|
485
|
-
headers["Authorization"] = f"Bearer {bearer_token}"
|
|
486
|
-
for env_header_name in server.env_http_headers:
|
|
487
|
-
env_value = os.environ.get(env_header_name)
|
|
488
|
-
if not env_value:
|
|
489
|
-
continue
|
|
490
|
-
if ":" in env_value:
|
|
491
|
-
key, value = env_value.split(":", 1)
|
|
492
|
-
headers[key.strip()] = value.strip()
|
|
493
|
-
else:
|
|
494
|
-
headers[env_header_name] = env_value
|
|
495
|
-
return headers, bearer_token
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
def _parse_tool_descriptor(
|
|
499
|
-
server_name: str, raw_tool: object
|
|
500
|
-
) -> MCPToolDescriptor | None:
|
|
501
|
-
if not isinstance(raw_tool, dict):
|
|
502
|
-
return None
|
|
503
|
-
tool_name = raw_tool.get("name")
|
|
504
|
-
if not isinstance(tool_name, str) or not tool_name.strip():
|
|
505
|
-
return None
|
|
506
|
-
raw_annotations = raw_tool.get("annotations")
|
|
507
|
-
annotations: dict[str, Any] = (
|
|
508
|
-
raw_annotations if isinstance(raw_annotations, dict) else {}
|
|
509
|
-
)
|
|
510
|
-
title = raw_tool.get("title")
|
|
511
|
-
description = raw_tool.get("description")
|
|
512
|
-
input_schema = raw_tool.get("inputSchema")
|
|
513
|
-
return MCPToolDescriptor(
|
|
514
|
-
server_name=server_name,
|
|
515
|
-
tool_name=tool_name.strip(),
|
|
516
|
-
fully_qualified_id=build_fully_qualified_tool_id(
|
|
517
|
-
server_name, tool_name.strip()
|
|
518
|
-
),
|
|
519
|
-
title=title.strip() if isinstance(title, str) and title.strip() else None,
|
|
520
|
-
description=description.strip() if isinstance(description, str) else "",
|
|
521
|
-
input_schema=input_schema if isinstance(input_schema, dict) else {},
|
|
522
|
-
read_only_hint=bool(
|
|
523
|
-
annotations.get("readOnlyHint", raw_tool.get("readOnlyHint", False))
|
|
524
|
-
),
|
|
525
|
-
destructive_hint=bool(
|
|
526
|
-
annotations.get(
|
|
527
|
-
"destructiveHint",
|
|
528
|
-
raw_tool.get("destructiveHint", False),
|
|
529
|
-
)
|
|
530
|
-
),
|
|
531
|
-
open_world_hint=bool(
|
|
532
|
-
annotations.get("openWorldHint", raw_tool.get("openWorldHint", False))
|
|
533
|
-
),
|
|
534
|
-
)
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
def _parse_resource_descriptor(
|
|
538
|
-
server_name: str,
|
|
539
|
-
raw_resource: object,
|
|
540
|
-
) -> MCPResourceDescriptor | None:
|
|
541
|
-
if not isinstance(raw_resource, dict):
|
|
542
|
-
return None
|
|
543
|
-
uri = raw_resource.get("uri")
|
|
544
|
-
name = raw_resource.get("name")
|
|
545
|
-
if not isinstance(uri, str) or not uri.strip():
|
|
546
|
-
return None
|
|
547
|
-
if not isinstance(name, str) or not name.strip():
|
|
548
|
-
name = uri
|
|
549
|
-
mime_type = raw_resource.get("mimeType")
|
|
550
|
-
description = raw_resource.get("description")
|
|
551
|
-
return MCPResourceDescriptor(
|
|
552
|
-
server_name=server_name,
|
|
553
|
-
name=name.strip(),
|
|
554
|
-
uri=uri.strip(),
|
|
555
|
-
mime_type=mime_type.strip()
|
|
556
|
-
if isinstance(mime_type, str) and mime_type.strip()
|
|
557
|
-
else None,
|
|
558
|
-
description=description.strip()
|
|
559
|
-
if isinstance(description, str) and description.strip()
|
|
560
|
-
else None,
|
|
561
|
-
)
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
def _parse_resource_template_descriptor(
|
|
565
|
-
server_name: str,
|
|
566
|
-
raw_template: object,
|
|
567
|
-
) -> MCPResourceTemplateDescriptor | None:
|
|
568
|
-
if not isinstance(raw_template, dict):
|
|
569
|
-
return None
|
|
570
|
-
uri_template = raw_template.get("uriTemplate")
|
|
571
|
-
name = raw_template.get("name")
|
|
572
|
-
if not isinstance(uri_template, str) or not uri_template.strip():
|
|
573
|
-
return None
|
|
574
|
-
if not isinstance(name, str) or not name.strip():
|
|
575
|
-
name = uri_template
|
|
576
|
-
description = raw_template.get("description")
|
|
577
|
-
return MCPResourceTemplateDescriptor(
|
|
578
|
-
server_name=server_name,
|
|
579
|
-
name=name.strip(),
|
|
580
|
-
uri_template=uri_template.strip(),
|
|
581
|
-
description=description.strip()
|
|
582
|
-
if isinstance(description, str) and description.strip()
|
|
583
|
-
else None,
|
|
584
|
-
)
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
def _parse_prompt_descriptor(
|
|
588
|
-
server_name: str, raw_prompt: object
|
|
589
|
-
) -> MCPPromptDescriptor | None:
|
|
590
|
-
if not isinstance(raw_prompt, dict):
|
|
591
|
-
return None
|
|
592
|
-
name = raw_prompt.get("name")
|
|
593
|
-
if not isinstance(name, str) or not name.strip():
|
|
594
|
-
return None
|
|
595
|
-
description = raw_prompt.get("description")
|
|
596
|
-
arguments = raw_prompt.get("arguments")
|
|
597
|
-
return MCPPromptDescriptor(
|
|
598
|
-
server_name=server_name,
|
|
599
|
-
name=name.strip(),
|
|
600
|
-
description=description.strip()
|
|
601
|
-
if isinstance(description, str) and description.strip()
|
|
602
|
-
else None,
|
|
603
|
-
arguments=[
|
|
604
|
-
dict(argument) for argument in arguments if isinstance(argument, dict)
|
|
605
|
-
]
|
|
606
|
-
if isinstance(arguments, list)
|
|
607
|
-
else [],
|
|
608
|
-
)
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
class _BaseConnection:
|
|
612
|
-
def __init__(self, server: MCPServerConfig, *, timeout_seconds: int) -> None:
|
|
613
|
-
self.server = server
|
|
614
|
-
self.timeout_seconds = timeout_seconds
|
|
615
|
-
self._next_request_id = 0
|
|
616
|
-
|
|
617
|
-
def _build_request(
|
|
618
|
-
self, method: str, params: dict[str, Any] | None = None
|
|
619
|
-
) -> dict[str, object]:
|
|
620
|
-
self._next_request_id += 1
|
|
621
|
-
payload: dict[str, object] = {
|
|
622
|
-
"jsonrpc": "2.0",
|
|
623
|
-
"id": self._next_request_id,
|
|
624
|
-
"method": method,
|
|
625
|
-
}
|
|
626
|
-
if params is not None:
|
|
627
|
-
payload["params"] = params
|
|
628
|
-
return payload
|
|
629
|
-
|
|
630
|
-
def _build_notification(
|
|
631
|
-
self, method: str, params: dict[str, Any] | None = None
|
|
632
|
-
) -> dict[str, object]:
|
|
633
|
-
payload: dict[str, object] = {
|
|
634
|
-
"jsonrpc": "2.0",
|
|
635
|
-
"method": method,
|
|
636
|
-
}
|
|
637
|
-
if params is not None:
|
|
638
|
-
payload["params"] = params
|
|
639
|
-
return payload
|
|
640
|
-
|
|
641
|
-
def initialize(self) -> dict[str, Any]:
|
|
642
|
-
result = self.request(
|
|
643
|
-
"initialize",
|
|
644
|
-
{
|
|
645
|
-
"protocolVersion": PROTOCOL_VERSION,
|
|
646
|
-
"capabilities": {
|
|
647
|
-
"roots": {"listChanged": False},
|
|
648
|
-
},
|
|
649
|
-
"clientInfo": {"name": "Flowent", "version": "dev"},
|
|
650
|
-
},
|
|
651
|
-
)
|
|
652
|
-
self.notify("notifications/initialized")
|
|
653
|
-
return result
|
|
654
|
-
|
|
655
|
-
def notify(self, method: str, params: dict[str, Any] | None = None) -> None:
|
|
656
|
-
self._send(self._build_notification(method, params))
|
|
657
|
-
|
|
658
|
-
def request(
|
|
659
|
-
self, method: str, params: dict[str, Any] | None = None
|
|
660
|
-
) -> dict[str, Any]:
|
|
661
|
-
payload = self._build_request(method, params)
|
|
662
|
-
request_id = payload["id"]
|
|
663
|
-
self._send(payload)
|
|
664
|
-
while True:
|
|
665
|
-
message = self._receive()
|
|
666
|
-
if not isinstance(message, dict):
|
|
667
|
-
continue
|
|
668
|
-
if message.get("id") == request_id and "result" in message:
|
|
669
|
-
result = message.get("result")
|
|
670
|
-
return result if isinstance(result, dict) else {}
|
|
671
|
-
if message.get("id") == request_id and "error" in message:
|
|
672
|
-
error = message.get("error")
|
|
673
|
-
raise MCPError(
|
|
674
|
-
error.get("message")
|
|
675
|
-
if isinstance(error, dict) and isinstance(error.get("message"), str)
|
|
676
|
-
else f"MCP request failed: {method}"
|
|
677
|
-
)
|
|
678
|
-
if "method" in message and "id" in message:
|
|
679
|
-
self._handle_server_request(message)
|
|
680
|
-
|
|
681
|
-
def close(self) -> None:
|
|
682
|
-
return None
|
|
683
|
-
|
|
684
|
-
def _handle_server_request(self, message: dict[str, Any]) -> None:
|
|
685
|
-
request_id = message.get("id")
|
|
686
|
-
method = message.get("method")
|
|
687
|
-
if not isinstance(request_id, int | str) or not isinstance(method, str):
|
|
688
|
-
return
|
|
689
|
-
if method == "roots/list":
|
|
690
|
-
self._send(
|
|
691
|
-
{
|
|
692
|
-
"jsonrpc": "2.0",
|
|
693
|
-
"id": request_id,
|
|
694
|
-
"result": {
|
|
695
|
-
"roots": self._build_roots(),
|
|
696
|
-
},
|
|
697
|
-
}
|
|
698
|
-
)
|
|
699
|
-
return
|
|
700
|
-
if method == "ping":
|
|
701
|
-
self._send({"jsonrpc": "2.0", "id": request_id, "result": {}})
|
|
702
|
-
return
|
|
703
|
-
self._send(
|
|
704
|
-
{
|
|
705
|
-
"jsonrpc": "2.0",
|
|
706
|
-
"id": request_id,
|
|
707
|
-
"error": {
|
|
708
|
-
"code": -32601,
|
|
709
|
-
"message": f"Unsupported MCP request: {method}",
|
|
710
|
-
},
|
|
711
|
-
}
|
|
712
|
-
)
|
|
713
|
-
|
|
714
|
-
def _build_roots(self) -> list[dict[str, str]]:
|
|
715
|
-
return []
|
|
716
|
-
|
|
717
|
-
def _send(self, payload: dict[str, object]) -> None:
|
|
718
|
-
raise NotImplementedError
|
|
719
|
-
|
|
720
|
-
def _receive(self) -> dict[str, Any]:
|
|
721
|
-
raise NotImplementedError
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
class _StdioConnection(_BaseConnection):
|
|
725
|
-
def __init__(
|
|
726
|
-
self,
|
|
727
|
-
server: MCPServerConfig,
|
|
728
|
-
*,
|
|
729
|
-
timeout_seconds: int,
|
|
730
|
-
roots: list[dict[str, str]],
|
|
731
|
-
) -> None:
|
|
732
|
-
super().__init__(server, timeout_seconds=timeout_seconds)
|
|
733
|
-
command: list[str] = [server.command, *server.args]
|
|
734
|
-
self._roots_payload = roots
|
|
735
|
-
self._stderr_lines: deque[str] = deque(maxlen=20)
|
|
736
|
-
self._stdout_queue: queue.Queue[dict[str, Any] | None] = queue.Queue()
|
|
737
|
-
try:
|
|
738
|
-
self._process = subprocess.Popen(
|
|
739
|
-
command,
|
|
740
|
-
cwd=server.cwd or None,
|
|
741
|
-
env=_build_stdio_env(server),
|
|
742
|
-
stdin=subprocess.PIPE,
|
|
743
|
-
stdout=subprocess.PIPE,
|
|
744
|
-
stderr=subprocess.PIPE,
|
|
745
|
-
text=True,
|
|
746
|
-
bufsize=1,
|
|
747
|
-
)
|
|
748
|
-
except FileNotFoundError as exc:
|
|
749
|
-
raise MCPError(str(exc)) from exc
|
|
750
|
-
self._stdout_thread = threading.Thread(target=self._read_stdout, daemon=True)
|
|
751
|
-
self._stderr_thread = threading.Thread(target=self._read_stderr, daemon=True)
|
|
752
|
-
self._stdout_thread.start()
|
|
753
|
-
self._stderr_thread.start()
|
|
754
|
-
|
|
755
|
-
def _build_roots(self) -> list[dict[str, str]]:
|
|
756
|
-
return list(self._roots_payload)
|
|
757
|
-
|
|
758
|
-
def _read_stdout(self) -> None:
|
|
759
|
-
assert self._process.stdout is not None
|
|
760
|
-
for line in self._process.stdout:
|
|
761
|
-
stripped = line.strip()
|
|
762
|
-
if not stripped:
|
|
763
|
-
continue
|
|
764
|
-
try:
|
|
765
|
-
payload = json.loads(stripped)
|
|
766
|
-
except json.JSONDecodeError:
|
|
767
|
-
self._stdout_queue.put(
|
|
768
|
-
{
|
|
769
|
-
"jsonrpc": "2.0",
|
|
770
|
-
"error": {
|
|
771
|
-
"message": f"Invalid MCP response: {stripped[:200]}",
|
|
772
|
-
},
|
|
773
|
-
}
|
|
774
|
-
)
|
|
775
|
-
continue
|
|
776
|
-
if isinstance(payload, dict):
|
|
777
|
-
self._stdout_queue.put(payload)
|
|
778
|
-
self._stdout_queue.put(None)
|
|
779
|
-
|
|
780
|
-
def _read_stderr(self) -> None:
|
|
781
|
-
assert self._process.stderr is not None
|
|
782
|
-
for line in self._process.stderr:
|
|
783
|
-
stripped = line.strip()
|
|
784
|
-
if stripped:
|
|
785
|
-
self._stderr_lines.append(stripped)
|
|
786
|
-
|
|
787
|
-
def _send(self, payload: dict[str, object]) -> None:
|
|
788
|
-
if self._process.stdin is None:
|
|
789
|
-
raise MCPError("MCP stdio connection is unavailable")
|
|
790
|
-
self._process.stdin.write(json.dumps(payload, ensure_ascii=False) + "\n")
|
|
791
|
-
self._process.stdin.flush()
|
|
792
|
-
|
|
793
|
-
def _receive(self) -> dict[str, Any]:
|
|
794
|
-
try:
|
|
795
|
-
message = self._stdout_queue.get(timeout=self.timeout_seconds)
|
|
796
|
-
except queue.Empty as exc:
|
|
797
|
-
stderr_tail = "\n".join(self._stderr_lines)
|
|
798
|
-
raise MCPError(
|
|
799
|
-
stderr_tail
|
|
800
|
-
or f"MCP stdio request timed out after {self.timeout_seconds}s"
|
|
801
|
-
) from exc
|
|
802
|
-
if message is None:
|
|
803
|
-
stderr_tail = "\n".join(self._stderr_lines)
|
|
804
|
-
raise MCPError(stderr_tail or "MCP stdio server closed the connection")
|
|
805
|
-
return message
|
|
806
|
-
|
|
807
|
-
def close(self) -> None:
|
|
808
|
-
if self._process.poll() is None:
|
|
809
|
-
self._process.terminate()
|
|
810
|
-
try:
|
|
811
|
-
self._process.wait(timeout=1)
|
|
812
|
-
except subprocess.TimeoutExpired:
|
|
813
|
-
self._process.kill()
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
class _HttpConnection(_BaseConnection):
|
|
817
|
-
def __init__(
|
|
818
|
-
self,
|
|
819
|
-
server: MCPServerConfig,
|
|
820
|
-
*,
|
|
821
|
-
timeout_seconds: int,
|
|
822
|
-
) -> None:
|
|
823
|
-
super().__init__(server, timeout_seconds=timeout_seconds)
|
|
824
|
-
headers, _ = _build_http_headers(server)
|
|
825
|
-
self._headers: dict[str, str] = headers
|
|
826
|
-
self._client: Any = curl_requests.Session()
|
|
827
|
-
self._pending_response: dict[str, Any] | list[dict[str, Any]] = {}
|
|
828
|
-
self._session_id: str | None = None
|
|
829
|
-
|
|
830
|
-
def _send(self, payload: dict[str, object]) -> None:
|
|
831
|
-
headers = {
|
|
832
|
-
"Accept": "application/json",
|
|
833
|
-
"Content-Type": "application/json",
|
|
834
|
-
"MCP-Protocol-Version": PROTOCOL_VERSION,
|
|
835
|
-
**self._headers,
|
|
836
|
-
}
|
|
837
|
-
if self._session_id:
|
|
838
|
-
headers["MCP-Session-Id"] = self._session_id
|
|
839
|
-
response = self._client.post(
|
|
840
|
-
self.server.url,
|
|
841
|
-
headers=headers,
|
|
842
|
-
json=payload,
|
|
843
|
-
timeout=self.timeout_seconds,
|
|
844
|
-
)
|
|
845
|
-
session_id = response.headers.get("MCP-Session-Id") or response.headers.get(
|
|
846
|
-
"mcp-session-id"
|
|
847
|
-
)
|
|
848
|
-
if session_id:
|
|
849
|
-
self._session_id = session_id
|
|
850
|
-
response.raise_for_status()
|
|
851
|
-
content_type = (response.headers.get("Content-Type") or "").lower()
|
|
852
|
-
response_text = response.text if getattr(response, "text", None) else ""
|
|
853
|
-
stripped_text = response_text.lstrip()
|
|
854
|
-
if (
|
|
855
|
-
"text/html" in content_type
|
|
856
|
-
or stripped_text.startswith("<!doctype html")
|
|
857
|
-
or stripped_text.startswith("<html")
|
|
858
|
-
):
|
|
859
|
-
raise MCPError(
|
|
860
|
-
"MCP request was blocked by an HTML challenge or interstitial response"
|
|
861
|
-
)
|
|
862
|
-
if "text/event-stream" in content_type:
|
|
863
|
-
self._pending_response = _parse_sse_payload(response_text)
|
|
864
|
-
return
|
|
865
|
-
raw_response = response.json() if response.content else {}
|
|
866
|
-
if isinstance(raw_response, list):
|
|
867
|
-
self._pending_response = [
|
|
868
|
-
item for item in raw_response if isinstance(item, dict)
|
|
869
|
-
]
|
|
870
|
-
return
|
|
871
|
-
if isinstance(raw_response, dict):
|
|
872
|
-
self._pending_response = raw_response
|
|
873
|
-
return
|
|
874
|
-
self._pending_response = {}
|
|
875
|
-
|
|
876
|
-
def _receive(self) -> dict[str, Any]:
|
|
877
|
-
payload = self._pending_response
|
|
878
|
-
if isinstance(payload, list):
|
|
879
|
-
for item in payload:
|
|
880
|
-
if isinstance(item, dict):
|
|
881
|
-
return item
|
|
882
|
-
raise MCPError("Invalid MCP HTTP response")
|
|
883
|
-
if not isinstance(payload, dict):
|
|
884
|
-
raise MCPError("Invalid MCP HTTP response")
|
|
885
|
-
return payload
|
|
886
|
-
|
|
887
|
-
def close(self) -> None:
|
|
888
|
-
if self._session_id:
|
|
889
|
-
with suppress(Exception):
|
|
890
|
-
self._client.delete(
|
|
891
|
-
self.server.url,
|
|
892
|
-
headers={"MCP-Session-Id": self._session_id},
|
|
893
|
-
timeout=self.timeout_seconds,
|
|
894
|
-
)
|
|
895
|
-
self._client.close()
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
def _list_paginated(
|
|
899
|
-
connection: _BaseConnection,
|
|
900
|
-
*,
|
|
901
|
-
method: str,
|
|
902
|
-
result_key: str,
|
|
903
|
-
) -> list[dict[str, Any]]:
|
|
904
|
-
items: list[dict[str, Any]] = []
|
|
905
|
-
cursor: str | None = None
|
|
906
|
-
while True:
|
|
907
|
-
params = {"cursor": cursor} if cursor else None
|
|
908
|
-
result = connection.request(method, params)
|
|
909
|
-
chunk = result.get(result_key)
|
|
910
|
-
if isinstance(chunk, list):
|
|
911
|
-
items.extend(item for item in chunk if isinstance(item, dict))
|
|
912
|
-
next_cursor = result.get("nextCursor")
|
|
913
|
-
if not isinstance(next_cursor, str) or not next_cursor.strip():
|
|
914
|
-
break
|
|
915
|
-
cursor = next_cursor
|
|
916
|
-
return items
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
def _parse_sse_payload(payload: str) -> list[dict[str, Any]]:
|
|
920
|
-
messages: list[dict[str, Any]] = []
|
|
921
|
-
data_lines: list[str] = []
|
|
922
|
-
for line in payload.splitlines():
|
|
923
|
-
if line.startswith("data:"):
|
|
924
|
-
data_lines.append(line.removeprefix("data:").strip())
|
|
925
|
-
continue
|
|
926
|
-
if line.strip():
|
|
927
|
-
continue
|
|
928
|
-
if not data_lines:
|
|
929
|
-
continue
|
|
930
|
-
try:
|
|
931
|
-
message = json.loads("\n".join(data_lines))
|
|
932
|
-
except json.JSONDecodeError:
|
|
933
|
-
data_lines = []
|
|
934
|
-
continue
|
|
935
|
-
if isinstance(message, dict):
|
|
936
|
-
messages.append(message)
|
|
937
|
-
data_lines = []
|
|
938
|
-
if data_lines:
|
|
939
|
-
try:
|
|
940
|
-
message = json.loads("\n".join(data_lines))
|
|
941
|
-
except json.JSONDecodeError:
|
|
942
|
-
return messages
|
|
943
|
-
if isinstance(message, dict):
|
|
944
|
-
messages.append(message)
|
|
945
|
-
return messages
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
def _auth_status_for_server(
|
|
949
|
-
server: MCPServerConfig,
|
|
950
|
-
*,
|
|
951
|
-
force_logged_out: bool = False,
|
|
952
|
-
) -> tuple[str, str | None]:
|
|
953
|
-
if server.transport == "stdio":
|
|
954
|
-
return "unsupported", None
|
|
955
|
-
if force_logged_out:
|
|
956
|
-
return ("not_logged_in", "MCP server session is logged out")
|
|
957
|
-
headers, bearer_token = _build_http_headers(server)
|
|
958
|
-
_ = headers
|
|
959
|
-
auth_expected = bool(
|
|
960
|
-
server.bearer_token_env_var or server.oauth_resource or server.scopes
|
|
961
|
-
)
|
|
962
|
-
if auth_expected and not bearer_token:
|
|
963
|
-
if server.bearer_token_env_var:
|
|
964
|
-
return (
|
|
965
|
-
"not_logged_in",
|
|
966
|
-
f"Set env var {server.bearer_token_env_var} before refreshing this server",
|
|
967
|
-
)
|
|
968
|
-
return (
|
|
969
|
-
"not_logged_in",
|
|
970
|
-
"Authentication is required before refreshing this server",
|
|
971
|
-
)
|
|
972
|
-
return ("connected" if auth_expected else "unsupported", None)
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
class MCPService:
|
|
976
|
-
def __init__(self) -> None:
|
|
977
|
-
self._lock = threading.Lock()
|
|
978
|
-
self._snapshots: dict[str, MCPDiscoverySnapshot] = {}
|
|
979
|
-
self._logged_out_servers: set[str] = set()
|
|
980
|
-
|
|
981
|
-
def reset(self) -> None:
|
|
982
|
-
with self._lock:
|
|
983
|
-
self._snapshots.clear()
|
|
984
|
-
self._logged_out_servers.clear()
|
|
985
|
-
connection = open_state_db(create=False)
|
|
986
|
-
if connection is None:
|
|
987
|
-
return
|
|
988
|
-
try:
|
|
989
|
-
with connection:
|
|
990
|
-
connection.execute("DELETE FROM mcp_snapshots")
|
|
991
|
-
connection.execute("DELETE FROM mcp_activities")
|
|
992
|
-
finally:
|
|
993
|
-
connection.close()
|
|
994
|
-
|
|
995
|
-
def clear_runtime_state(self) -> None:
|
|
996
|
-
with self._lock:
|
|
997
|
-
self._snapshots.clear()
|
|
998
|
-
self._logged_out_servers.clear()
|
|
999
|
-
|
|
1000
|
-
def bootstrap(self) -> None:
|
|
1001
|
-
settings = get_settings()
|
|
1002
|
-
for server in settings.mcp_servers:
|
|
1003
|
-
if not server.enabled:
|
|
1004
|
-
self._set_snapshot(
|
|
1005
|
-
MCPDiscoverySnapshot(
|
|
1006
|
-
server_name=server.name,
|
|
1007
|
-
transport=server.transport,
|
|
1008
|
-
status="disabled",
|
|
1009
|
-
auth_status="unsupported"
|
|
1010
|
-
if server.transport == "stdio"
|
|
1011
|
-
else "not_logged_in",
|
|
1012
|
-
)
|
|
1013
|
-
)
|
|
1014
|
-
continue
|
|
1015
|
-
try:
|
|
1016
|
-
self.refresh_server(server.name)
|
|
1017
|
-
except MCPError as exc:
|
|
1018
|
-
if server.required:
|
|
1019
|
-
raise RuntimeError(str(exc)) from exc
|
|
1020
|
-
|
|
1021
|
-
def _prune_activities_locked(self, connection, now: float) -> None:
|
|
1022
|
-
min_timestamp = now - ACTIVITY_RETENTION_SECONDS
|
|
1023
|
-
connection.execute(
|
|
1024
|
-
"DELETE FROM mcp_activities WHERE ended_at < ?",
|
|
1025
|
-
(min_timestamp,),
|
|
1026
|
-
)
|
|
1027
|
-
|
|
1028
|
-
def _record_activity(
|
|
1029
|
-
self,
|
|
1030
|
-
*,
|
|
1031
|
-
server_name: str,
|
|
1032
|
-
action: str,
|
|
1033
|
-
actor_node_id: str | None,
|
|
1034
|
-
tab_id: str | None,
|
|
1035
|
-
started_at: float,
|
|
1036
|
-
ended_at: float,
|
|
1037
|
-
result: str,
|
|
1038
|
-
summary: str,
|
|
1039
|
-
tool_name: str | None = None,
|
|
1040
|
-
fully_qualified_id: str | None = None,
|
|
1041
|
-
target: str | None = None,
|
|
1042
|
-
approval_result: str | None = None,
|
|
1043
|
-
) -> None:
|
|
1044
|
-
record = MCPActivityRecord(
|
|
1045
|
-
id=str(uuid.uuid4()),
|
|
1046
|
-
server_name=server_name,
|
|
1047
|
-
action=action,
|
|
1048
|
-
actor_node_id=actor_node_id,
|
|
1049
|
-
tab_id=tab_id,
|
|
1050
|
-
started_at=started_at,
|
|
1051
|
-
ended_at=ended_at,
|
|
1052
|
-
result=result,
|
|
1053
|
-
summary=summary,
|
|
1054
|
-
tool_name=tool_name,
|
|
1055
|
-
fully_qualified_id=fully_qualified_id,
|
|
1056
|
-
target=target,
|
|
1057
|
-
approval_result=approval_result,
|
|
1058
|
-
)
|
|
1059
|
-
with self._lock:
|
|
1060
|
-
connection = open_state_db(create=True)
|
|
1061
|
-
assert connection is not None
|
|
1062
|
-
try:
|
|
1063
|
-
with connection:
|
|
1064
|
-
self._prune_activities_locked(connection, ended_at)
|
|
1065
|
-
connection.execute(
|
|
1066
|
-
"""
|
|
1067
|
-
INSERT INTO mcp_activities (
|
|
1068
|
-
id,
|
|
1069
|
-
server_name,
|
|
1070
|
-
ended_at,
|
|
1071
|
-
payload
|
|
1072
|
-
) VALUES (?, ?, ?, ?)
|
|
1073
|
-
""",
|
|
1074
|
-
(
|
|
1075
|
-
record.id,
|
|
1076
|
-
record.server_name,
|
|
1077
|
-
record.ended_at,
|
|
1078
|
-
json.dumps(record.serialize(), ensure_ascii=False),
|
|
1079
|
-
),
|
|
1080
|
-
)
|
|
1081
|
-
finally:
|
|
1082
|
-
connection.close()
|
|
1083
|
-
|
|
1084
|
-
def _persist_snapshot_locked(self, snapshot: MCPDiscoverySnapshot) -> None:
|
|
1085
|
-
connection = open_state_db(create=True)
|
|
1086
|
-
assert connection is not None
|
|
1087
|
-
try:
|
|
1088
|
-
with connection:
|
|
1089
|
-
connection.execute(
|
|
1090
|
-
"""
|
|
1091
|
-
INSERT OR REPLACE INTO mcp_snapshots (server_name, payload)
|
|
1092
|
-
VALUES (?, ?)
|
|
1093
|
-
""",
|
|
1094
|
-
(
|
|
1095
|
-
snapshot.server_name,
|
|
1096
|
-
json.dumps(snapshot.serialize(), ensure_ascii=False),
|
|
1097
|
-
),
|
|
1098
|
-
)
|
|
1099
|
-
finally:
|
|
1100
|
-
connection.close()
|
|
1101
|
-
|
|
1102
|
-
def _delete_snapshot_locked(self, server_name: str) -> None:
|
|
1103
|
-
connection = open_state_db(create=False)
|
|
1104
|
-
if connection is None:
|
|
1105
|
-
return
|
|
1106
|
-
try:
|
|
1107
|
-
with connection:
|
|
1108
|
-
connection.execute(
|
|
1109
|
-
"DELETE FROM mcp_snapshots WHERE server_name = ?",
|
|
1110
|
-
(server_name,),
|
|
1111
|
-
)
|
|
1112
|
-
finally:
|
|
1113
|
-
connection.close()
|
|
1114
|
-
|
|
1115
|
-
def _load_snapshot_from_db_locked(
|
|
1116
|
-
self,
|
|
1117
|
-
server_name: str,
|
|
1118
|
-
) -> MCPDiscoverySnapshot | None:
|
|
1119
|
-
connection = open_state_db(create=False)
|
|
1120
|
-
if connection is None:
|
|
1121
|
-
return None
|
|
1122
|
-
try:
|
|
1123
|
-
row = connection.execute(
|
|
1124
|
-
"SELECT payload FROM mcp_snapshots WHERE server_name = ?",
|
|
1125
|
-
(server_name,),
|
|
1126
|
-
).fetchone()
|
|
1127
|
-
finally:
|
|
1128
|
-
connection.close()
|
|
1129
|
-
if row is None:
|
|
1130
|
-
return None
|
|
1131
|
-
payload = row["payload"]
|
|
1132
|
-
if not isinstance(payload, str):
|
|
1133
|
-
return None
|
|
1134
|
-
parsed = json.loads(payload)
|
|
1135
|
-
if not isinstance(parsed, dict):
|
|
1136
|
-
return None
|
|
1137
|
-
return _snapshot_from_mapping(parsed)
|
|
1138
|
-
|
|
1139
|
-
def _set_snapshot(self, snapshot: MCPDiscoverySnapshot) -> None:
|
|
1140
|
-
with self._lock:
|
|
1141
|
-
self._snapshots[snapshot.server_name] = snapshot
|
|
1142
|
-
self._persist_snapshot_locked(snapshot)
|
|
1143
|
-
|
|
1144
|
-
def _get_snapshot(self, server_name: str) -> MCPDiscoverySnapshot | None:
|
|
1145
|
-
with self._lock:
|
|
1146
|
-
snapshot = self._snapshots.get(server_name)
|
|
1147
|
-
if snapshot is not None:
|
|
1148
|
-
return snapshot
|
|
1149
|
-
snapshot = self._load_snapshot_from_db_locked(server_name)
|
|
1150
|
-
if snapshot is not None:
|
|
1151
|
-
self._snapshots[server_name] = snapshot
|
|
1152
|
-
return snapshot
|
|
1153
|
-
|
|
1154
|
-
def _build_connection(
|
|
1155
|
-
self,
|
|
1156
|
-
server: MCPServerConfig,
|
|
1157
|
-
*,
|
|
1158
|
-
timeout_seconds: int,
|
|
1159
|
-
roots: list[dict[str, str]] | None = None,
|
|
1160
|
-
) -> _BaseConnection:
|
|
1161
|
-
if server.transport == "stdio":
|
|
1162
|
-
if not server.command.strip():
|
|
1163
|
-
raise MCPError(f"MCP server '{server.name}' is missing a command")
|
|
1164
|
-
return _StdioConnection(
|
|
1165
|
-
server,
|
|
1166
|
-
timeout_seconds=timeout_seconds,
|
|
1167
|
-
roots=roots or [],
|
|
1168
|
-
)
|
|
1169
|
-
if not server.url.strip():
|
|
1170
|
-
raise MCPError(f"MCP server '{server.name}' is missing a URL")
|
|
1171
|
-
return _HttpConnection(server, timeout_seconds=timeout_seconds)
|
|
1172
|
-
|
|
1173
|
-
def _discover_server(
|
|
1174
|
-
self,
|
|
1175
|
-
server: MCPServerConfig,
|
|
1176
|
-
*,
|
|
1177
|
-
auth_result: str | None = None,
|
|
1178
|
-
) -> MCPDiscoverySnapshot:
|
|
1179
|
-
auth_status, auth_error = _auth_status_for_server(
|
|
1180
|
-
server,
|
|
1181
|
-
force_logged_out=server.name in self._logged_out_servers,
|
|
1182
|
-
)
|
|
1183
|
-
if server.transport == "streamable_http" and auth_status == "not_logged_in":
|
|
1184
|
-
return MCPDiscoverySnapshot(
|
|
1185
|
-
server_name=server.name,
|
|
1186
|
-
transport=server.transport,
|
|
1187
|
-
status="auth_required",
|
|
1188
|
-
auth_status=auth_status,
|
|
1189
|
-
last_auth_result=auth_result,
|
|
1190
|
-
last_refresh_at=time.time(),
|
|
1191
|
-
last_refresh_result="error",
|
|
1192
|
-
last_error=auth_error,
|
|
1193
|
-
)
|
|
1194
|
-
|
|
1195
|
-
connection = self._build_connection(
|
|
1196
|
-
server,
|
|
1197
|
-
timeout_seconds=server.startup_timeout_sec,
|
|
1198
|
-
)
|
|
1199
|
-
try:
|
|
1200
|
-
connection.initialize()
|
|
1201
|
-
tools = [
|
|
1202
|
-
descriptor
|
|
1203
|
-
for descriptor in (
|
|
1204
|
-
_parse_tool_descriptor(server.name, raw_tool)
|
|
1205
|
-
for raw_tool in _list_paginated(
|
|
1206
|
-
connection,
|
|
1207
|
-
method="tools/list",
|
|
1208
|
-
result_key="tools",
|
|
1209
|
-
)
|
|
1210
|
-
)
|
|
1211
|
-
if descriptor is not None
|
|
1212
|
-
]
|
|
1213
|
-
if server.enabled_tools:
|
|
1214
|
-
tools = [
|
|
1215
|
-
descriptor
|
|
1216
|
-
for descriptor in tools
|
|
1217
|
-
if descriptor.tool_name in server.enabled_tools
|
|
1218
|
-
]
|
|
1219
|
-
if server.disabled_tools:
|
|
1220
|
-
disabled_tool_names = set(server.disabled_tools)
|
|
1221
|
-
tools = [
|
|
1222
|
-
descriptor
|
|
1223
|
-
for descriptor in tools
|
|
1224
|
-
if descriptor.tool_name not in disabled_tool_names
|
|
1225
|
-
]
|
|
1226
|
-
resources = [
|
|
1227
|
-
descriptor
|
|
1228
|
-
for descriptor in (
|
|
1229
|
-
_parse_resource_descriptor(server.name, raw_resource)
|
|
1230
|
-
for raw_resource in _list_paginated(
|
|
1231
|
-
connection,
|
|
1232
|
-
method="resources/list",
|
|
1233
|
-
result_key="resources",
|
|
1234
|
-
)
|
|
1235
|
-
)
|
|
1236
|
-
if descriptor is not None
|
|
1237
|
-
]
|
|
1238
|
-
resource_templates = [
|
|
1239
|
-
descriptor
|
|
1240
|
-
for descriptor in (
|
|
1241
|
-
_parse_resource_template_descriptor(server.name, raw_template)
|
|
1242
|
-
for raw_template in _list_paginated(
|
|
1243
|
-
connection,
|
|
1244
|
-
method="resources/templates/list",
|
|
1245
|
-
result_key="resourceTemplates",
|
|
1246
|
-
)
|
|
1247
|
-
)
|
|
1248
|
-
if descriptor is not None
|
|
1249
|
-
]
|
|
1250
|
-
prompts = [
|
|
1251
|
-
descriptor
|
|
1252
|
-
for descriptor in (
|
|
1253
|
-
_parse_prompt_descriptor(server.name, raw_prompt)
|
|
1254
|
-
for raw_prompt in _list_paginated(
|
|
1255
|
-
connection,
|
|
1256
|
-
method="prompts/list",
|
|
1257
|
-
result_key="prompts",
|
|
1258
|
-
)
|
|
1259
|
-
)
|
|
1260
|
-
if descriptor is not None
|
|
1261
|
-
]
|
|
1262
|
-
return MCPDiscoverySnapshot(
|
|
1263
|
-
server_name=server.name,
|
|
1264
|
-
transport=server.transport,
|
|
1265
|
-
status="connected",
|
|
1266
|
-
auth_status=auth_status,
|
|
1267
|
-
last_auth_result=auth_result,
|
|
1268
|
-
last_refresh_at=time.time(),
|
|
1269
|
-
last_refresh_result="success",
|
|
1270
|
-
tools=tools,
|
|
1271
|
-
resources=resources,
|
|
1272
|
-
resource_templates=resource_templates,
|
|
1273
|
-
prompts=prompts,
|
|
1274
|
-
)
|
|
1275
|
-
finally:
|
|
1276
|
-
connection.close()
|
|
1277
|
-
|
|
1278
|
-
def list_server_payloads(self) -> list[dict[str, object]]:
|
|
1279
|
-
settings = get_settings()
|
|
1280
|
-
payloads: list[dict[str, object]] = []
|
|
1281
|
-
for server in settings.mcp_servers:
|
|
1282
|
-
snapshot = self._get_snapshot(server.name)
|
|
1283
|
-
server_payload: dict[str, object] = {
|
|
1284
|
-
"config": {
|
|
1285
|
-
"name": server.name,
|
|
1286
|
-
"transport": server.transport,
|
|
1287
|
-
"enabled": server.enabled,
|
|
1288
|
-
"required": server.required,
|
|
1289
|
-
"startup_timeout_sec": server.startup_timeout_sec,
|
|
1290
|
-
"tool_timeout_sec": server.tool_timeout_sec,
|
|
1291
|
-
"enabled_tools": list(server.enabled_tools),
|
|
1292
|
-
"disabled_tools": list(server.disabled_tools),
|
|
1293
|
-
"scopes": list(server.scopes),
|
|
1294
|
-
"oauth_resource": server.oauth_resource,
|
|
1295
|
-
"launcher": server.launcher,
|
|
1296
|
-
"command": server.command,
|
|
1297
|
-
"args": list(server.args),
|
|
1298
|
-
"env": dict(server.env),
|
|
1299
|
-
"env_vars": list(server.env_vars),
|
|
1300
|
-
"cwd": server.cwd,
|
|
1301
|
-
"url": server.url,
|
|
1302
|
-
"bearer_token_env_var": server.bearer_token_env_var,
|
|
1303
|
-
"http_headers": dict(server.http_headers),
|
|
1304
|
-
"env_http_headers": list(server.env_http_headers),
|
|
1305
|
-
},
|
|
1306
|
-
"snapshot": snapshot.serialize()
|
|
1307
|
-
if snapshot is not None
|
|
1308
|
-
else MCPDiscoverySnapshot(
|
|
1309
|
-
server_name=server.name,
|
|
1310
|
-
transport=server.transport,
|
|
1311
|
-
status="disabled" if not server.enabled else "connecting",
|
|
1312
|
-
auth_status="unsupported"
|
|
1313
|
-
if server.transport == "stdio"
|
|
1314
|
-
else "not_logged_in",
|
|
1315
|
-
).serialize(),
|
|
1316
|
-
"visibility": {
|
|
1317
|
-
"scope": "global",
|
|
1318
|
-
"active": snapshot is not None and snapshot.status == "connected",
|
|
1319
|
-
},
|
|
1320
|
-
"activity": [
|
|
1321
|
-
activity.serialize()
|
|
1322
|
-
for activity in self.list_activities(server_name=server.name)
|
|
1323
|
-
],
|
|
1324
|
-
}
|
|
1325
|
-
payloads.append(server_payload)
|
|
1326
|
-
return payloads
|
|
1327
|
-
|
|
1328
|
-
def list_activities(
|
|
1329
|
-
self, *, server_name: str | None = None
|
|
1330
|
-
) -> list[MCPActivityRecord]:
|
|
1331
|
-
with self._lock:
|
|
1332
|
-
connection = open_state_db(create=False)
|
|
1333
|
-
if connection is None:
|
|
1334
|
-
return []
|
|
1335
|
-
try:
|
|
1336
|
-
with connection:
|
|
1337
|
-
self._prune_activities_locked(connection, time.time())
|
|
1338
|
-
if server_name is None:
|
|
1339
|
-
rows = connection.execute(
|
|
1340
|
-
"""
|
|
1341
|
-
SELECT payload
|
|
1342
|
-
FROM mcp_activities
|
|
1343
|
-
ORDER BY ended_at DESC
|
|
1344
|
-
"""
|
|
1345
|
-
).fetchall()
|
|
1346
|
-
else:
|
|
1347
|
-
rows = connection.execute(
|
|
1348
|
-
"""
|
|
1349
|
-
SELECT payload
|
|
1350
|
-
FROM mcp_activities
|
|
1351
|
-
WHERE server_name = ?
|
|
1352
|
-
ORDER BY ended_at DESC
|
|
1353
|
-
""",
|
|
1354
|
-
(server_name,),
|
|
1355
|
-
).fetchall()
|
|
1356
|
-
finally:
|
|
1357
|
-
connection.close()
|
|
1358
|
-
records: list[MCPActivityRecord] = []
|
|
1359
|
-
for row in rows:
|
|
1360
|
-
payload = row["payload"]
|
|
1361
|
-
if not isinstance(payload, str):
|
|
1362
|
-
continue
|
|
1363
|
-
parsed = json.loads(payload)
|
|
1364
|
-
if not isinstance(parsed, dict):
|
|
1365
|
-
continue
|
|
1366
|
-
record = _activity_from_mapping(parsed)
|
|
1367
|
-
if record is not None:
|
|
1368
|
-
records.append(record)
|
|
1369
|
-
return records
|
|
1370
|
-
|
|
1371
|
-
def refresh_server(self, server_name: str) -> dict[str, object]:
|
|
1372
|
-
settings = get_settings()
|
|
1373
|
-
server = find_mcp_server(settings, server_name)
|
|
1374
|
-
if server is None:
|
|
1375
|
-
raise MCPError(f"MCP server '{server_name}' not found")
|
|
1376
|
-
if not server.enabled:
|
|
1377
|
-
snapshot = MCPDiscoverySnapshot(
|
|
1378
|
-
server_name=server.name,
|
|
1379
|
-
transport=server.transport,
|
|
1380
|
-
status="disabled",
|
|
1381
|
-
auth_status="unsupported"
|
|
1382
|
-
if server.transport == "stdio"
|
|
1383
|
-
else "not_logged_in",
|
|
1384
|
-
last_auth_result=None,
|
|
1385
|
-
last_refresh_at=time.time(),
|
|
1386
|
-
last_refresh_result="success",
|
|
1387
|
-
)
|
|
1388
|
-
self._set_snapshot(snapshot)
|
|
1389
|
-
return snapshot.serialize()
|
|
1390
|
-
started_at = time.time()
|
|
1391
|
-
try:
|
|
1392
|
-
previous_snapshot = self._get_snapshot(server.name)
|
|
1393
|
-
snapshot = self._discover_server(server)
|
|
1394
|
-
if snapshot.last_auth_result is None and previous_snapshot is not None:
|
|
1395
|
-
snapshot.last_auth_result = previous_snapshot.last_auth_result
|
|
1396
|
-
self._set_snapshot(snapshot)
|
|
1397
|
-
self._record_activity(
|
|
1398
|
-
server_name=server.name,
|
|
1399
|
-
action="refresh",
|
|
1400
|
-
actor_node_id=None,
|
|
1401
|
-
tab_id=None,
|
|
1402
|
-
started_at=started_at,
|
|
1403
|
-
ended_at=time.time(),
|
|
1404
|
-
result="success"
|
|
1405
|
-
if snapshot.last_refresh_result == "success"
|
|
1406
|
-
else "error",
|
|
1407
|
-
summary=(
|
|
1408
|
-
"Capabilities refreshed"
|
|
1409
|
-
if snapshot.last_refresh_result == "success"
|
|
1410
|
-
else snapshot.last_error or "Failed to refresh capabilities"
|
|
1411
|
-
),
|
|
1412
|
-
)
|
|
1413
|
-
return snapshot.serialize()
|
|
1414
|
-
except Exception as exc:
|
|
1415
|
-
snapshot = MCPDiscoverySnapshot(
|
|
1416
|
-
server_name=server.name,
|
|
1417
|
-
transport=server.transport,
|
|
1418
|
-
status="error",
|
|
1419
|
-
auth_status="error"
|
|
1420
|
-
if server.transport == "streamable_http"
|
|
1421
|
-
else "unsupported",
|
|
1422
|
-
last_auth_result=None,
|
|
1423
|
-
last_refresh_at=time.time(),
|
|
1424
|
-
last_refresh_result="error",
|
|
1425
|
-
last_error=str(exc),
|
|
1426
|
-
)
|
|
1427
|
-
self._set_snapshot(snapshot)
|
|
1428
|
-
self._record_activity(
|
|
1429
|
-
server_name=server.name,
|
|
1430
|
-
action="refresh",
|
|
1431
|
-
actor_node_id=None,
|
|
1432
|
-
tab_id=None,
|
|
1433
|
-
started_at=started_at,
|
|
1434
|
-
ended_at=time.time(),
|
|
1435
|
-
result="error",
|
|
1436
|
-
summary=str(exc),
|
|
1437
|
-
)
|
|
1438
|
-
raise MCPError(str(exc)) from exc
|
|
1439
|
-
|
|
1440
|
-
def refresh_all(self) -> list[dict[str, object]]:
|
|
1441
|
-
results: list[dict[str, object]] = []
|
|
1442
|
-
for server in get_settings().mcp_servers:
|
|
1443
|
-
try:
|
|
1444
|
-
results.append(self.refresh_server(server.name))
|
|
1445
|
-
except MCPError:
|
|
1446
|
-
snapshot = self._get_snapshot(server.name)
|
|
1447
|
-
if snapshot is not None:
|
|
1448
|
-
results.append(snapshot.serialize())
|
|
1449
|
-
return results
|
|
1450
|
-
|
|
1451
|
-
def create_or_update_server(
|
|
1452
|
-
self,
|
|
1453
|
-
*,
|
|
1454
|
-
current_name: str | None,
|
|
1455
|
-
config_data: dict[str, object],
|
|
1456
|
-
) -> dict[str, object]:
|
|
1457
|
-
settings = get_settings()
|
|
1458
|
-
normalized_name = config_data.get("name")
|
|
1459
|
-
if not isinstance(normalized_name, str) or not normalized_name.strip():
|
|
1460
|
-
raise MCPError("name must not be empty")
|
|
1461
|
-
next_name = normalized_name.strip()
|
|
1462
|
-
existing = find_mcp_server(settings, next_name)
|
|
1463
|
-
if existing is not None and existing.name != current_name:
|
|
1464
|
-
raise MCPError(f"MCP server '{next_name}' already exists")
|
|
1465
|
-
from flowent.settings import _build_mcp_server_config
|
|
1466
|
-
|
|
1467
|
-
server_config, migrated = _build_mcp_server_config(config_data)
|
|
1468
|
-
_ = migrated
|
|
1469
|
-
if server_config is None:
|
|
1470
|
-
raise MCPError("Invalid MCP server config")
|
|
1471
|
-
if current_name is not None and current_name != next_name:
|
|
1472
|
-
with self._lock:
|
|
1473
|
-
self._snapshots.pop(current_name, None)
|
|
1474
|
-
self._logged_out_servers.discard(current_name)
|
|
1475
|
-
self._delete_snapshot_locked(current_name)
|
|
1476
|
-
replaced = False
|
|
1477
|
-
for index, existing_server in enumerate(settings.mcp_servers):
|
|
1478
|
-
if existing_server.name != (current_name or next_name):
|
|
1479
|
-
continue
|
|
1480
|
-
settings.mcp_servers[index] = server_config
|
|
1481
|
-
replaced = True
|
|
1482
|
-
break
|
|
1483
|
-
if not replaced:
|
|
1484
|
-
settings.mcp_servers.append(server_config)
|
|
1485
|
-
save_settings(settings)
|
|
1486
|
-
if not server_config.enabled:
|
|
1487
|
-
self._logged_out_servers.discard(server_config.name)
|
|
1488
|
-
snapshot = MCPDiscoverySnapshot(
|
|
1489
|
-
server_name=server_config.name,
|
|
1490
|
-
transport=server_config.transport,
|
|
1491
|
-
status="disabled",
|
|
1492
|
-
auth_status="unsupported"
|
|
1493
|
-
if server_config.transport == "stdio"
|
|
1494
|
-
else "not_logged_in",
|
|
1495
|
-
last_auth_result=None,
|
|
1496
|
-
last_refresh_at=time.time(),
|
|
1497
|
-
last_refresh_result="success",
|
|
1498
|
-
)
|
|
1499
|
-
self._set_snapshot(snapshot)
|
|
1500
|
-
return snapshot.serialize()
|
|
1501
|
-
self._logged_out_servers.discard(server_config.name)
|
|
1502
|
-
return self.refresh_server(server_config.name)
|
|
1503
|
-
|
|
1504
|
-
def delete_server(self, server_name: str) -> None:
|
|
1505
|
-
settings = get_settings()
|
|
1506
|
-
if find_mcp_server(settings, server_name) is None:
|
|
1507
|
-
raise MCPError(f"MCP server '{server_name}' not found")
|
|
1508
|
-
settings.mcp_servers = [
|
|
1509
|
-
server for server in settings.mcp_servers if server.name != server_name
|
|
1510
|
-
]
|
|
1511
|
-
save_settings(settings)
|
|
1512
|
-
with self._lock:
|
|
1513
|
-
self._snapshots.pop(server_name, None)
|
|
1514
|
-
self._logged_out_servers.discard(server_name)
|
|
1515
|
-
self._delete_snapshot_locked(server_name)
|
|
1516
|
-
|
|
1517
|
-
def login_server(self, server_name: str) -> dict[str, object]:
|
|
1518
|
-
self._logged_out_servers.discard(server_name)
|
|
1519
|
-
started_at = time.time()
|
|
1520
|
-
snapshot = self.refresh_server(server_name)
|
|
1521
|
-
current_snapshot = self._get_snapshot(server_name)
|
|
1522
|
-
if current_snapshot is not None:
|
|
1523
|
-
current_snapshot.last_auth_result = (
|
|
1524
|
-
"success" if current_snapshot.auth_status == "connected" else "error"
|
|
1525
|
-
)
|
|
1526
|
-
self._set_snapshot(current_snapshot)
|
|
1527
|
-
self._record_activity(
|
|
1528
|
-
server_name=server_name,
|
|
1529
|
-
action="login",
|
|
1530
|
-
actor_node_id=None,
|
|
1531
|
-
tab_id=None,
|
|
1532
|
-
started_at=started_at,
|
|
1533
|
-
ended_at=time.time(),
|
|
1534
|
-
result="success"
|
|
1535
|
-
if current_snapshot.auth_status == "connected"
|
|
1536
|
-
else "error",
|
|
1537
|
-
summary=(
|
|
1538
|
-
"Logged in MCP server session"
|
|
1539
|
-
if current_snapshot.auth_status == "connected"
|
|
1540
|
-
else current_snapshot.last_error or "Failed to login MCP server"
|
|
1541
|
-
),
|
|
1542
|
-
)
|
|
1543
|
-
return current_snapshot.serialize()
|
|
1544
|
-
return snapshot
|
|
1545
|
-
|
|
1546
|
-
def logout_server(self, server_name: str) -> dict[str, object]:
|
|
1547
|
-
settings = get_settings()
|
|
1548
|
-
server = find_mcp_server(settings, server_name)
|
|
1549
|
-
if server is None:
|
|
1550
|
-
raise MCPError(f"MCP server '{server_name}' not found")
|
|
1551
|
-
if server.transport == "stdio":
|
|
1552
|
-
raise MCPError("Logout is not available for stdio MCP servers")
|
|
1553
|
-
self._logged_out_servers.add(server_name)
|
|
1554
|
-
snapshot = MCPDiscoverySnapshot(
|
|
1555
|
-
server_name=server.name,
|
|
1556
|
-
transport=server.transport,
|
|
1557
|
-
status="auth_required",
|
|
1558
|
-
auth_status="not_logged_in",
|
|
1559
|
-
last_auth_result="logged_out",
|
|
1560
|
-
last_refresh_at=time.time(),
|
|
1561
|
-
last_refresh_result="success",
|
|
1562
|
-
last_error=None,
|
|
1563
|
-
)
|
|
1564
|
-
self._set_snapshot(snapshot)
|
|
1565
|
-
self._record_activity(
|
|
1566
|
-
server_name=server_name,
|
|
1567
|
-
action="logout",
|
|
1568
|
-
actor_node_id=None,
|
|
1569
|
-
tab_id=None,
|
|
1570
|
-
started_at=time.time(),
|
|
1571
|
-
ended_at=time.time(),
|
|
1572
|
-
result="success",
|
|
1573
|
-
summary="Logged out MCP server session",
|
|
1574
|
-
)
|
|
1575
|
-
return snapshot.serialize()
|
|
1576
|
-
|
|
1577
|
-
def _visible_server_names_for_agent(self, agent: Agent) -> list[str]:
|
|
1578
|
-
from flowent.graph_service import resolve_effective_permissions_for_agent
|
|
1579
|
-
|
|
1580
|
-
settings = get_settings()
|
|
1581
|
-
allow_network, _ = resolve_effective_permissions_for_agent(agent)
|
|
1582
|
-
visible_names: list[str] = []
|
|
1583
|
-
seen: set[str] = set()
|
|
1584
|
-
for server in settings.mcp_servers:
|
|
1585
|
-
if server.name in seen or not server.enabled:
|
|
1586
|
-
continue
|
|
1587
|
-
if server.transport == "streamable_http" and not allow_network:
|
|
1588
|
-
continue
|
|
1589
|
-
snapshot = self._get_snapshot(server.name)
|
|
1590
|
-
if snapshot is None or snapshot.status != "connected":
|
|
1591
|
-
continue
|
|
1592
|
-
seen.add(server.name)
|
|
1593
|
-
visible_names.append(server.name)
|
|
1594
|
-
return visible_names
|
|
1595
|
-
|
|
1596
|
-
def _visible_snapshots_for_agent(self, agent: Agent) -> list[MCPDiscoverySnapshot]:
|
|
1597
|
-
snapshots: list[MCPDiscoverySnapshot] = []
|
|
1598
|
-
for server_name in self._visible_server_names_for_agent(agent):
|
|
1599
|
-
snapshot = self._get_snapshot(server_name)
|
|
1600
|
-
if snapshot is None or snapshot.status != "connected":
|
|
1601
|
-
continue
|
|
1602
|
-
snapshots.append(snapshot)
|
|
1603
|
-
return snapshots
|
|
1604
|
-
|
|
1605
|
-
def list_discovered_tool_descriptors(self) -> list[dict[str, object]]:
|
|
1606
|
-
return [descriptor.serialize() for descriptor in self.list_discovered_tools()]
|
|
1607
|
-
|
|
1608
|
-
def list_discovered_tools(self) -> list[MCPToolDescriptor]:
|
|
1609
|
-
tools: list[MCPToolDescriptor] = []
|
|
1610
|
-
with self._lock:
|
|
1611
|
-
snapshots = list(self._snapshots.values())
|
|
1612
|
-
for snapshot in snapshots:
|
|
1613
|
-
if snapshot.status != "connected":
|
|
1614
|
-
continue
|
|
1615
|
-
tools.extend(snapshot.tools)
|
|
1616
|
-
return tools
|
|
1617
|
-
|
|
1618
|
-
def get_dynamic_tool_descriptor(
|
|
1619
|
-
self,
|
|
1620
|
-
fully_qualified_id: str,
|
|
1621
|
-
) -> MCPToolDescriptor | None:
|
|
1622
|
-
for descriptor in self.list_discovered_tools():
|
|
1623
|
-
if descriptor.fully_qualified_id == fully_qualified_id:
|
|
1624
|
-
return descriptor
|
|
1625
|
-
return None
|
|
1626
|
-
|
|
1627
|
-
def list_agent_dynamic_tools(self, agent: Agent) -> list[MCPToolDescriptor]:
|
|
1628
|
-
from flowent.models import NodeType
|
|
1629
|
-
from flowent.tools import is_assistant_only_mcp_tool_name
|
|
1630
|
-
|
|
1631
|
-
tools: list[MCPToolDescriptor] = []
|
|
1632
|
-
for snapshot in self._visible_snapshots_for_agent(agent):
|
|
1633
|
-
tools.extend(
|
|
1634
|
-
descriptor
|
|
1635
|
-
for descriptor in snapshot.tools
|
|
1636
|
-
if agent.node_type == NodeType.ASSISTANT
|
|
1637
|
-
or not is_assistant_only_mcp_tool_name(descriptor.tool_name)
|
|
1638
|
-
)
|
|
1639
|
-
return tools
|
|
1640
|
-
|
|
1641
|
-
def has_visible_capabilities(self, agent: Agent) -> bool:
|
|
1642
|
-
return bool(self._visible_snapshots_for_agent(agent))
|
|
1643
|
-
|
|
1644
|
-
def list_agent_resources(
|
|
1645
|
-
self,
|
|
1646
|
-
agent: Agent,
|
|
1647
|
-
*,
|
|
1648
|
-
server_name: str | None = None,
|
|
1649
|
-
) -> list[dict[str, object]]:
|
|
1650
|
-
resources: list[dict[str, object]] = []
|
|
1651
|
-
for snapshot in self._visible_snapshots_for_agent(agent):
|
|
1652
|
-
if server_name is not None and snapshot.server_name != server_name:
|
|
1653
|
-
continue
|
|
1654
|
-
resources.extend(item.serialize() for item in snapshot.resources)
|
|
1655
|
-
return resources
|
|
1656
|
-
|
|
1657
|
-
def list_agent_resource_templates(
|
|
1658
|
-
self,
|
|
1659
|
-
agent: Agent,
|
|
1660
|
-
*,
|
|
1661
|
-
server_name: str | None = None,
|
|
1662
|
-
) -> list[dict[str, object]]:
|
|
1663
|
-
templates: list[dict[str, object]] = []
|
|
1664
|
-
for snapshot in self._visible_snapshots_for_agent(agent):
|
|
1665
|
-
if server_name is not None and snapshot.server_name != server_name:
|
|
1666
|
-
continue
|
|
1667
|
-
templates.extend(item.serialize() for item in snapshot.resource_templates)
|
|
1668
|
-
return templates
|
|
1669
|
-
|
|
1670
|
-
def list_agent_prompts(
|
|
1671
|
-
self,
|
|
1672
|
-
agent: Agent,
|
|
1673
|
-
*,
|
|
1674
|
-
server_name: str | None = None,
|
|
1675
|
-
) -> list[dict[str, object]]:
|
|
1676
|
-
prompts: list[dict[str, object]] = []
|
|
1677
|
-
for snapshot in self._visible_snapshots_for_agent(agent):
|
|
1678
|
-
if server_name is not None and snapshot.server_name != server_name:
|
|
1679
|
-
continue
|
|
1680
|
-
prompts.extend(item.serialize() for item in snapshot.prompts)
|
|
1681
|
-
return prompts
|
|
1682
|
-
|
|
1683
|
-
def _get_server_for_agent(self, agent: Agent, server_name: str) -> MCPServerConfig:
|
|
1684
|
-
from flowent.graph_service import resolve_effective_permissions_for_agent
|
|
1685
|
-
|
|
1686
|
-
if server_name not in self._visible_server_names_for_agent(agent):
|
|
1687
|
-
raise MCPError(f"MCP server '{server_name}' is not globally available")
|
|
1688
|
-
server = find_mcp_server(get_settings(), server_name)
|
|
1689
|
-
if server is None or not server.enabled:
|
|
1690
|
-
raise MCPError(f"MCP server '{server_name}' is unavailable")
|
|
1691
|
-
allow_network, _ = resolve_effective_permissions_for_agent(agent)
|
|
1692
|
-
if server.transport == "streamable_http" and not allow_network:
|
|
1693
|
-
raise MCPError("Network access is disabled for this workflow")
|
|
1694
|
-
return server
|
|
1695
|
-
|
|
1696
|
-
def read_agent_resource(
|
|
1697
|
-
self,
|
|
1698
|
-
agent: Agent,
|
|
1699
|
-
*,
|
|
1700
|
-
server_name: str,
|
|
1701
|
-
uri: str,
|
|
1702
|
-
) -> dict[str, Any]:
|
|
1703
|
-
server = self._get_server_for_agent(agent, server_name)
|
|
1704
|
-
started_at = time.time()
|
|
1705
|
-
connection = self._build_connection(
|
|
1706
|
-
server,
|
|
1707
|
-
timeout_seconds=server.tool_timeout_sec,
|
|
1708
|
-
roots=_build_roots_for_agent(agent),
|
|
1709
|
-
)
|
|
1710
|
-
try:
|
|
1711
|
-
connection.initialize()
|
|
1712
|
-
result = connection.request("resources/read", {"uri": uri})
|
|
1713
|
-
self._record_activity(
|
|
1714
|
-
server_name=server_name,
|
|
1715
|
-
action="resource_read",
|
|
1716
|
-
actor_node_id=agent.uuid,
|
|
1717
|
-
tab_id=agent.config.tab_id,
|
|
1718
|
-
started_at=started_at,
|
|
1719
|
-
ended_at=time.time(),
|
|
1720
|
-
result="success",
|
|
1721
|
-
summary=f"Read resource {uri}",
|
|
1722
|
-
target=uri,
|
|
1723
|
-
)
|
|
1724
|
-
return result
|
|
1725
|
-
except Exception as exc:
|
|
1726
|
-
self._record_activity(
|
|
1727
|
-
server_name=server_name,
|
|
1728
|
-
action="resource_read",
|
|
1729
|
-
actor_node_id=agent.uuid,
|
|
1730
|
-
tab_id=agent.config.tab_id,
|
|
1731
|
-
started_at=started_at,
|
|
1732
|
-
ended_at=time.time(),
|
|
1733
|
-
result="error",
|
|
1734
|
-
summary=str(exc),
|
|
1735
|
-
target=uri,
|
|
1736
|
-
)
|
|
1737
|
-
raise MCPError(str(exc)) from exc
|
|
1738
|
-
finally:
|
|
1739
|
-
connection.close()
|
|
1740
|
-
|
|
1741
|
-
def get_agent_prompt(
|
|
1742
|
-
self,
|
|
1743
|
-
agent: Agent,
|
|
1744
|
-
*,
|
|
1745
|
-
server_name: str,
|
|
1746
|
-
name: str,
|
|
1747
|
-
arguments: dict[str, Any] | None = None,
|
|
1748
|
-
) -> dict[str, Any]:
|
|
1749
|
-
server = self._get_server_for_agent(agent, server_name)
|
|
1750
|
-
started_at = time.time()
|
|
1751
|
-
connection = self._build_connection(
|
|
1752
|
-
server,
|
|
1753
|
-
timeout_seconds=server.tool_timeout_sec,
|
|
1754
|
-
roots=_build_roots_for_agent(agent),
|
|
1755
|
-
)
|
|
1756
|
-
try:
|
|
1757
|
-
connection.initialize()
|
|
1758
|
-
params: dict[str, Any] = {"name": name}
|
|
1759
|
-
if arguments:
|
|
1760
|
-
params["arguments"] = arguments
|
|
1761
|
-
result = connection.request("prompts/get", params)
|
|
1762
|
-
self._record_activity(
|
|
1763
|
-
server_name=server_name,
|
|
1764
|
-
action="prompt_get",
|
|
1765
|
-
actor_node_id=agent.uuid,
|
|
1766
|
-
tab_id=agent.config.tab_id,
|
|
1767
|
-
started_at=started_at,
|
|
1768
|
-
ended_at=time.time(),
|
|
1769
|
-
result="success",
|
|
1770
|
-
summary=f"Loaded prompt {name}",
|
|
1771
|
-
target=name,
|
|
1772
|
-
)
|
|
1773
|
-
return result
|
|
1774
|
-
except Exception as exc:
|
|
1775
|
-
self._record_activity(
|
|
1776
|
-
server_name=server_name,
|
|
1777
|
-
action="prompt_get",
|
|
1778
|
-
actor_node_id=agent.uuid,
|
|
1779
|
-
tab_id=agent.config.tab_id,
|
|
1780
|
-
started_at=started_at,
|
|
1781
|
-
ended_at=time.time(),
|
|
1782
|
-
result="error",
|
|
1783
|
-
summary=str(exc),
|
|
1784
|
-
target=name,
|
|
1785
|
-
)
|
|
1786
|
-
raise MCPError(str(exc)) from exc
|
|
1787
|
-
finally:
|
|
1788
|
-
connection.close()
|
|
1789
|
-
|
|
1790
|
-
def preview_server_prompt(
|
|
1791
|
-
self,
|
|
1792
|
-
*,
|
|
1793
|
-
server_name: str,
|
|
1794
|
-
name: str,
|
|
1795
|
-
arguments: dict[str, Any] | None = None,
|
|
1796
|
-
) -> dict[str, Any]:
|
|
1797
|
-
settings = get_settings()
|
|
1798
|
-
server = find_mcp_server(settings, server_name)
|
|
1799
|
-
if server is None or not server.enabled:
|
|
1800
|
-
raise MCPError(f"MCP server '{server_name}' is unavailable")
|
|
1801
|
-
started_at = time.time()
|
|
1802
|
-
connection = self._build_connection(
|
|
1803
|
-
server,
|
|
1804
|
-
timeout_seconds=server.tool_timeout_sec,
|
|
1805
|
-
)
|
|
1806
|
-
try:
|
|
1807
|
-
connection.initialize()
|
|
1808
|
-
params: dict[str, Any] = {"name": name}
|
|
1809
|
-
if arguments:
|
|
1810
|
-
params["arguments"] = arguments
|
|
1811
|
-
result = connection.request("prompts/get", params)
|
|
1812
|
-
self._record_activity(
|
|
1813
|
-
server_name=server_name,
|
|
1814
|
-
action="prompt_get",
|
|
1815
|
-
actor_node_id=None,
|
|
1816
|
-
tab_id=None,
|
|
1817
|
-
started_at=started_at,
|
|
1818
|
-
ended_at=time.time(),
|
|
1819
|
-
result="success",
|
|
1820
|
-
summary=f"Previewed prompt {name}",
|
|
1821
|
-
target=name,
|
|
1822
|
-
)
|
|
1823
|
-
return result
|
|
1824
|
-
except Exception as exc:
|
|
1825
|
-
self._record_activity(
|
|
1826
|
-
server_name=server_name,
|
|
1827
|
-
action="prompt_get",
|
|
1828
|
-
actor_node_id=None,
|
|
1829
|
-
tab_id=None,
|
|
1830
|
-
started_at=started_at,
|
|
1831
|
-
ended_at=time.time(),
|
|
1832
|
-
result="error",
|
|
1833
|
-
summary=str(exc),
|
|
1834
|
-
target=name,
|
|
1835
|
-
)
|
|
1836
|
-
raise MCPError(str(exc)) from exc
|
|
1837
|
-
finally:
|
|
1838
|
-
connection.close()
|
|
1839
|
-
|
|
1840
|
-
def call_agent_tool(
|
|
1841
|
-
self,
|
|
1842
|
-
agent: Agent,
|
|
1843
|
-
*,
|
|
1844
|
-
fully_qualified_id: str,
|
|
1845
|
-
arguments: dict[str, Any],
|
|
1846
|
-
) -> dict[str, Any]:
|
|
1847
|
-
descriptor = next(
|
|
1848
|
-
(
|
|
1849
|
-
item
|
|
1850
|
-
for item in self.list_agent_dynamic_tools(agent)
|
|
1851
|
-
if item.fully_qualified_id == fully_qualified_id
|
|
1852
|
-
),
|
|
1853
|
-
None,
|
|
1854
|
-
)
|
|
1855
|
-
if descriptor is None:
|
|
1856
|
-
raise MCPError(f"MCP tool '{fully_qualified_id}' is not available")
|
|
1857
|
-
if descriptor.destructive_hint or descriptor.open_world_hint:
|
|
1858
|
-
self._record_activity(
|
|
1859
|
-
server_name=descriptor.server_name,
|
|
1860
|
-
action="tool_call",
|
|
1861
|
-
actor_node_id=agent.uuid,
|
|
1862
|
-
tab_id=agent.config.tab_id,
|
|
1863
|
-
started_at=time.time(),
|
|
1864
|
-
ended_at=time.time(),
|
|
1865
|
-
result="rejected",
|
|
1866
|
-
summary="Explicit MCP approval is required for this tool",
|
|
1867
|
-
tool_name=descriptor.tool_name,
|
|
1868
|
-
fully_qualified_id=descriptor.fully_qualified_id,
|
|
1869
|
-
approval_result="requires_approval",
|
|
1870
|
-
)
|
|
1871
|
-
raise MCPError("Explicit MCP approval is required for this tool")
|
|
1872
|
-
server = self._get_server_for_agent(agent, descriptor.server_name)
|
|
1873
|
-
started_at = time.time()
|
|
1874
|
-
connection = self._build_connection(
|
|
1875
|
-
server,
|
|
1876
|
-
timeout_seconds=server.tool_timeout_sec,
|
|
1877
|
-
roots=_build_roots_for_agent(agent),
|
|
1878
|
-
)
|
|
1879
|
-
try:
|
|
1880
|
-
connection.initialize()
|
|
1881
|
-
result = connection.request(
|
|
1882
|
-
"tools/call",
|
|
1883
|
-
{"name": descriptor.tool_name, "arguments": arguments},
|
|
1884
|
-
)
|
|
1885
|
-
self._record_activity(
|
|
1886
|
-
server_name=descriptor.server_name,
|
|
1887
|
-
action="tool_call",
|
|
1888
|
-
actor_node_id=agent.uuid,
|
|
1889
|
-
tab_id=agent.config.tab_id,
|
|
1890
|
-
started_at=started_at,
|
|
1891
|
-
ended_at=time.time(),
|
|
1892
|
-
result="success",
|
|
1893
|
-
summary=f"Called tool {descriptor.tool_name}",
|
|
1894
|
-
tool_name=descriptor.tool_name,
|
|
1895
|
-
fully_qualified_id=descriptor.fully_qualified_id,
|
|
1896
|
-
approval_result="granted",
|
|
1897
|
-
)
|
|
1898
|
-
return result
|
|
1899
|
-
except Exception as exc:
|
|
1900
|
-
self._record_activity(
|
|
1901
|
-
server_name=descriptor.server_name,
|
|
1902
|
-
action="tool_call",
|
|
1903
|
-
actor_node_id=agent.uuid,
|
|
1904
|
-
tab_id=agent.config.tab_id,
|
|
1905
|
-
started_at=started_at,
|
|
1906
|
-
ended_at=time.time(),
|
|
1907
|
-
result="error",
|
|
1908
|
-
summary=str(exc),
|
|
1909
|
-
tool_name=descriptor.tool_name,
|
|
1910
|
-
fully_qualified_id=descriptor.fully_qualified_id,
|
|
1911
|
-
approval_result="granted",
|
|
1912
|
-
)
|
|
1913
|
-
raise MCPError(str(exc)) from exc
|
|
1914
|
-
finally:
|
|
1915
|
-
connection.close()
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
mcp_service = MCPService()
|