Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
bd27f38
refactor(api): wire tool provider management through application serv…
41tair Oct 8, 2026
8474a47
Merge updated stack base and resolve conflicts in refactor/api-76-10-10
41tair Oct 8, 2026
9e263f6
Merge updated stack base into refactor/api-76-10-10
41tair Oct 8, 2026
899b052
test: align tool provider fixtures with repository-backed services
41tair Oct 9, 2026
797b421
Merge updated stack base into refactor/api-76-10-10
41tair Oct 9, 2026
b8587ae
Merge updated refactor/api-76-10-9
41tair Oct 15, 2026
fca3baa
Merge updated refactor/api-76-10-9 into refactor/api-76-10-10
41tair Oct 19, 2026
314d5bc
Merge updated refactor/api-76-10-9 into refactor/api-76-10-10
41tair Oct 19, 2026
f39f581
Merge updated refactor/api-76-10-9 into refactor/api-76-10-10
41tair Oct 19, 2026
d2cea91
Merge updated refactor/api-76-10-9 into refactor/api-76-10-10
41tair Oct 19, 2026
ba137b4
Merge updated refactor/api-76-10-10 parent with Graphon 0.8 compatibi…
41tair Oct 20, 2026
fd0b0b5
Merge workflow migration compatibility from updated parent
41tair Oct 20, 2026
e7981f8
Merge execution test compatibility from updated parent
41tair Oct 20, 2026
4c8bde0
Merge updated refactor/api-76-10-9 into refactor/api-76-10-10
41tair Oct 26, 2026
57c0e44
Sync validated refactor/api-76-10-9 fixes
41tair Oct 26, 2026
dc73285
Sync validated refactor/api-76-10-9 fixes
41tair Oct 26, 2026
a0f674a
Sync validated refactor/api-76-10-9 fixes
41tair Oct 26, 2026
ccedfdd
Sync validated refactor/api-76-10-9 fixes
41tair Oct 26, 2026
fb83525
Sync validated refactor/api-76-10-9 fixes
41tair Oct 26, 2026
358d869
Sync validated refactor/api-76-10-9 fixes
41tair Oct 26, 2026
017cfcd
Sync validated refactor/api-76-10-9 fixes
41tair Oct 27, 2026
1beba6c
Sync validated refactor/api-76-10-9 fixes
41tair Oct 27, 2026
6eb9782
Sync validated refactor/api-76-10-9 fixes
41tair Oct 27, 2026
3cecf0c
[autofix.ci] apply automated fixes
autofix-ci[bot] Oct 27, 2026
9bcf7ab
Merge updated refactor/api-76-10-9 into refactor/api-76-10-10
41tair Oct 31, 2026
7edb669
Sync validated refactor/api-76-10-9 fixes
41tair Oct 31, 2026
a7283d4
Sync validated refactor/api-76-10-9 fixes
41tair Oct 31, 2026
462a1fa
Sync validated refactor/api-76-10-9 fixes
41tair Oct 31, 2026
5d2b573
Sync validated refactor/api-76-10-9 fixes
41tair Oct 31, 2026
c4c5041
Sync validated refactor/api-76-10-9 fixes
41tair Oct 31, 2026
ff66d9d
Sync validated refactor/api-76-10-9 fixes
41tair Oct 31, 2026
78acb20
Sync validated refactor/api-76-10-9 fixes
41tair Oct 31, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion api/.importlinter
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ ignore_imports =
core.app.task_pipeline.message_cycle_manager -> services.annotation_service
core.helper.credential_utils -> services.enterprise.plugin_manager_service
core.helper.credential_utils -> services.system_feature_service
core.mcp.auth_client -> services.tools.legacy_mcp_tools_manage_service
core.mcp.auth_client -> services.tools.mcp_tools_manage_service
core.provider_manager -> services.credit_pool_service
core.provider_manager -> services.feature_service
core.rag.datasource.retrieval_service -> services.knowledge.external.service
Expand Down
14 changes: 7 additions & 7 deletions api/controllers/console/workspace/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,9 @@
from flask_restx import Resource
from pydantic import BaseModel, ConfigDict, Field, RootModel
from werkzeug.datastructures import FileStorage
from werkzeug.exceptions import Forbidden

from configs import dify_config
from controllers.common.errors import InvalidArgumentError
from controllers.common.errors import AccessDeniedError, InvalidArgumentError
from controllers.common.fields import BinaryFileResponse, SuccessResponse
from controllers.common.rbac import RBACCheck, Workspace
from controllers.common.schema import (
Expand Down Expand Up @@ -49,7 +48,7 @@
from core.tools.entities.api_entities import ToolProviderApiEntity
from core.tools.entities.common_entities import I18nObject
from core.tools.entities.tool_entities import ToolProviderType
from core.tools.tool_manager import ToolManager
from extensions.ext_application_services import application_services
from extensions.ext_database import db
from fields.base import ResponseModel
from graphon.model_runtime.utils.encoders import jsonable_encoder
Expand All @@ -71,7 +70,8 @@
from services.plugin.plugin_auto_upgrade_service import PluginAutoUpgradeService
from services.plugin.plugin_parameter_service import PluginParameterService
from services.plugin.plugin_permission_service import PluginPermissionService
from services.tools.legacy_tools_transform_service import ToolTransformService
from services.tools.tool_manager import ToolManager
from services.tools.tools_transform_service import ToolTransformService

_PLUGIN_PACKAGE_UPLOAD_PARAMS = {
"pkg": {
Expand Down Expand Up @@ -537,7 +537,7 @@ def _list_hardcoded_builtin_tool_providers(
"""List builtin providers using the same search and tag semantics as category plugins."""
db_builtin_providers = {
str(ToolProviderID(provider.provider)): provider
for provider in ToolManager.list_default_builtin_providers(tenant_id)
for provider in application_services().tools.tool_providers.default_builtin(tenant_id=tenant_id)
}
builtin_providers = []

Expand Down Expand Up @@ -1116,7 +1116,7 @@ class PluginChangePermissionApi(Resource):
@model_validate(ParserPermissionChange)
def post(self, req_data: ParserPermissionChange, tenant_id: str, user: Account):
if not user.is_admin_or_owner:
raise Forbidden()
raise AccessDeniedError()

set_permission_result = PluginPermissionService.change_permission(
tenant_id, req_data.install_permission, req_data.debug_permission, session=db.session()
Expand Down Expand Up @@ -1229,7 +1229,7 @@ class PluginChangeAutoUpgradeApi(Resource):
@model_validate(ParserAutoUpgradeChange)
def post(self, req_data: ParserAutoUpgradeChange, tenant_id: str, user: Account):
if not dify_config.RBAC_ENABLED and not user.is_admin_or_owner:
raise Forbidden()
raise AccessDeniedError()

auto_upgrade = req_data.auto_upgrade
set_auto_upgrade_strategy_result = PluginAutoUpgradeService.change_strategy(
Expand Down
92 changes: 49 additions & 43 deletions api/controllers/console/workspace/tool_providers.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@
model_validator,
)
from sqlalchemy.orm import sessionmaker
from werkzeug.exceptions import Forbidden

from configs import dify_config
from controllers.common.errors import AccessDeniedError
from controllers.common.fields import SimpleResultResponse
from controllers.common.rbac import RBACCheck, Workspace
from controllers.common.schema import (
Expand Down Expand Up @@ -73,13 +73,11 @@

# from models.provider_ids import ToolProviderID
from services.plugin.oauth_service import OAuthProxyService
from services.tools.legacy_api_tools_manage_service import ApiToolManageService, ApiToolPreviewResult
from services.tools.legacy_builtin_tools_manage_service import BuiltinToolManageService
from services.tools.legacy_mcp_tools_manage_service import MCPToolManageService, OAuthDataType
from services.tools.legacy_tools_manage_service import ToolCommonService
from services.tools.legacy_tools_transform_service import ToolTransformService
from services.tools.legacy_workflow_tools_manage_service import WorkflowToolManageService
from services.tools.api_tools_manage_service import ApiToolManageService, ApiToolPreviewResult
from services.tools.builtin_tools_manage_service import BuiltinToolManageService
from services.tools.mcp_tools_manage_service import MCPToolManageService, OAuthDataType
from services.tools.tool_labels_service import ToolLabelsService
from services.tools.tools_manage_service import ToolCommonService

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -508,7 +506,11 @@ def get(self, tenant_id: str, user: Account):

return _dump_tool_provider_payload_list(
ToolCommonService.list_tool_providers(
user.id, tenant_id, cast(ToolProviderTypeApiLiteral | None, query.type)
user.id,
tenant_id,
cast(ToolProviderTypeApiLiteral | None, query.type),
workflow_queries=application_services().tools.workflow_queries,
tool_providers=application_services().tools.tool_providers,
),
)

Expand Down Expand Up @@ -753,9 +755,7 @@ def get(self, tenant_id: str, user: Account):
return dump_response(
ToolApiListResponse,
ApiToolManageService.list_api_tool_provider_tools(
user.id,
tenant_id,
query.provider,
user.id, tenant_id, query.provider, tool_providers=application_services().tools.tool_providers
),
)

Expand Down Expand Up @@ -831,9 +831,7 @@ def get(self, tenant_id: str, user: Account):
return dump_response(
ApiProviderDetailResponse,
ApiToolManageService.get_api_tool_provider(
user.id,
tenant_id,
query.provider,
user.id, tenant_id, query.provider, tool_providers=application_services().tools.tool_providers
),
)

Expand Down Expand Up @@ -913,7 +911,7 @@ class ToolWorkflowProviderCreateApi(Resource):
def post(self, req_data: WorkflowToolCreatePayload, tenant_id: str, user: Account):
return dump_response(
SimpleResultResponse,
WorkflowToolManageService.create_workflow_tool(
application_services().tools.workflows.create_workflow_tool(
user_id=user.id,
tenant_id=tenant_id,
workflow_app_id=req_data.workflow_app_id,
Expand Down Expand Up @@ -943,7 +941,7 @@ class ToolWorkflowProviderUpdateApi(Resource):
def post(self, req_data: WorkflowToolUpdatePayload, tenant_id: str, user: Account):
return dump_response(
SimpleResultResponse,
WorkflowToolManageService.update_workflow_tool(
application_services().tools.workflows.update_workflow_tool(
user.id,
tenant_id,
req_data.workflow_tool_id,
Expand Down Expand Up @@ -973,7 +971,7 @@ class ToolWorkflowProviderDeleteApi(Resource):
def post(self, req_data: WorkflowToolDeletePayload, tenant_id: str, user: Account):
return dump_response(
SimpleResultResponse,
WorkflowToolManageService.delete_workflow_tool(
application_services().tools.workflows.delete_workflow_tool(
user.id,
tenant_id,
req_data.workflow_tool_id,
Expand All @@ -996,13 +994,13 @@ def get(self, tenant_id: str, user: Account):
query = query_params_from_request(WorkflowToolGetQuery)

if query.workflow_tool_id:
tool = WorkflowToolManageService.get_workflow_tool_by_tool_id(
tool = application_services().tools.workflows.get_workflow_tool_by_tool_id(
user.id,
tenant_id,
query.workflow_tool_id,
)
elif query.workflow_app_id:
tool = WorkflowToolManageService.get_workflow_tool_by_app_id(
tool = application_services().tools.workflows.get_workflow_tool_by_app_id(
user.id,
tenant_id,
query.workflow_app_id,
Expand All @@ -1029,7 +1027,7 @@ def get(self, tenant_id: str, user: Account):

return dump_response(
ToolApiListResponse,
WorkflowToolManageService.list_single_workflow_tools(
application_services().tools.workflows.list_single_workflow_tools(
user.id,
tenant_id,
query.workflow_tool_id,
Expand All @@ -1052,8 +1050,7 @@ def get(self, tenant_id: str, user: Account):
[
provider.to_dict()
for provider in BuiltinToolManageService.list_builtin_tools(
user.id,
tenant_id,
user.id, tenant_id, tool_providers=application_services().tools.tool_providers
)
],
)
Expand All @@ -1071,7 +1068,7 @@ def get(self, tenant_id: str):
[
provider.to_dict()
for provider in ApiToolManageService.list_api_tools(
tenant_id,
tenant_id, tool_providers=application_services().tools.tool_providers
)
],
)
Expand All @@ -1091,7 +1088,7 @@ def get(self, tenant_id: str, user: Account):
return _dump_tool_provider_payload_list(
[
provider.to_dict()
for provider in WorkflowToolManageService.list_tenant_workflow_tools(
for provider in application_services().tools.workflows.list_tenant_workflow_tools(
user.id,
tenant_id,
)
Expand Down Expand Up @@ -1130,9 +1127,11 @@ def get(self, tenant_id: str, user: Account, provider: str):
plugin_id = tool_provider.plugin_id
provider_name = tool_provider.provider_name

oauth_client_params = BuiltinToolManageService.get_oauth_client(tenant_id=tenant_id, provider=provider)
oauth_client_params = BuiltinToolManageService.get_oauth_client(
tenant_id=tenant_id, provider=provider, tool_providers=application_services().tools.tool_providers
)
if oauth_client_params is None:
raise Forbidden("no oauth available client config found for this tool provider")
raise AccessDeniedError("no oauth available client config found for this tool provider")

# Visibility is chosen by the user in the frontend before the redirect,
# then read back in the callback below when the credential is created.
Expand Down Expand Up @@ -1186,11 +1185,11 @@ class ToolOAuthCallback(Resource):
def get(self, provider: str):
context_id = request.cookies.get("context_id")
if not context_id:
raise Forbidden("context_id not found")
raise AccessDeniedError("context_id not found")

context = OAuthProxyService.use_proxy_context(context_id)
if context is None:
raise Forbidden("Invalid context_id")
raise AccessDeniedError("Invalid context_id")

tool_provider = ToolProviderID(provider)
plugin_id = tool_provider.plugin_id
Expand All @@ -1199,9 +1198,11 @@ def get(self, provider: str):
tenant_id: str = context["tenant_id"]

oauth_handler = OAuthHandler()
oauth_client_params = BuiltinToolManageService.get_oauth_client(tenant_id, provider)
oauth_client_params = BuiltinToolManageService.get_oauth_client(
tenant_id, provider, tool_providers=application_services().tools.tool_providers
)
if oauth_client_params is None:
raise Forbidden("no oauth available client config found for this tool provider")
raise AccessDeniedError("no oauth available client config found for this tool provider")

try:
OAuthProxyService.validate_plugin_context(
Expand All @@ -1211,7 +1212,7 @@ def get(self, provider: str):
plugin_unique_identifier=oauth_client_params.plugin_unique_identifier,
)
except ValueError as error:
raise Forbidden(str(error)) from error
raise AccessDeniedError(str(error)) from error

redirect_uri = f"{dify_config.CONSOLE_API_URL}/console/api/oauth/plugin/{provider}/tool/callback"
credentials_response = oauth_handler.get_credentials(
Expand Down Expand Up @@ -1401,7 +1402,7 @@ def post(self, req_data: MCPProviderCreatePayload, tenant_id: str, user: Account
# 1) Create provider in a short transaction (no network I/O inside)
with session_factory.create_session() as session, session.begin():
service = MCPToolManageService(session=session)
result = service.create_provider(
provider_id = service.create_provider(
tenant_id=tenant_id,
user_id=user.id,
server_url=req_data.server_url,
Expand All @@ -1416,6 +1417,10 @@ def post(self, req_data: MCPProviderCreatePayload, tenant_id: str, user: Account
identity_mode=_resolve_identity_mode(req_data.identity_mode, current=IdentityMode.OFF),
)

result = MCPToolManageService.provider_response(
tenant_id=tenant_id, provider_id=provider_id, tool_providers=application_services().tools.tool_providers
)

# 2) Try to fetch tools immediately after creation so they appear without a second save.
# Perform network I/O outside any DB session to avoid holding locks.
try:
Expand All @@ -1432,7 +1437,9 @@ def post(self, req_data: MCPProviderCreatePayload, tenant_id: str, user: Account
db_provider.authed = reconnect.authed
db_provider.tools = reconnect.tools

result = ToolTransformService.mcp_provider_to_user_provider(db_provider)
result = MCPToolManageService.provider_response(
tenant_id=tenant_id, provider_id=result.id, tool_providers=application_services().tools.tool_providers
)
except Exception:
# Best-effort: if initial fetch fails (e.g., auth required), return created provider as-is
logger.warning("Failed to fetch MCP tools after creation", exc_info=True)
Expand Down Expand Up @@ -1596,10 +1603,10 @@ class ToolMCPDetailApi(Resource):
@rbac_permission_required(RBACCheck(RBACPermission.MCP_MANAGE, Workspace()))
@with_current_tenant_id
def get(self, tenant_id: str, provider_id: str):
with sessionmaker(db.engine).begin() as session:
service = MCPToolManageService(session=session)
provider = service.get_provider_by_id(provider_id=provider_id, tenant_id=tenant_id)
return _dump_tool_provider_payload(ToolTransformService.mcp_provider_to_user_provider(provider).to_dict())
provider = MCPToolManageService.provider_response(
tenant_id=tenant_id, provider_id=provider_id, tool_providers=application_services().tools.tool_providers
)
return _dump_tool_provider_payload(provider.to_dict())


@console_ns.route("/workspaces/current/tools/mcp")
Expand All @@ -1610,12 +1617,11 @@ class ToolMCPListAllApi(Resource):
@account_initialization_required
@with_current_tenant_id
def get(self, tenant_id: str):
with sessionmaker(db.engine).begin() as session:
service = MCPToolManageService(session=session)
# Skip sensitive data decryption for list view to improve performance
tools = service.list_providers(tenant_id=tenant_id, include_sensitive=False)

return _dump_tool_provider_payload_list([tool.to_dict() for tool in tools])
# Skip sensitive data decryption for list view to improve performance.
tools = MCPToolManageService.list_providers(
tenant_id=tenant_id, include_sensitive=False, tool_providers=application_services().tools.tool_providers
)
return _dump_tool_provider_payload_list([tool.to_dict() for tool in tools])


@console_ns.route("/workspaces/current/tool-provider/mcp/update/<path:provider_id>")
Expand Down
Loading
Loading