diff --git a/api/.env.example b/api/.env.example
index 01fcad599e..099b9f25fa 100644
--- a/api/.env.example
+++ b/api/.env.example
@@ -17,6 +17,11 @@ APP_WEB_URL=http://127.0.0.1:3000
# Files URL
FILES_URL=http://127.0.0.1:5001
+# INTERNAL_FILES_URL is used for plugin daemon communication within Docker network.
+# Set this to the internal Docker service URL for proper plugin file access.
+# Example: INTERNAL_FILES_URL=http://api:5001
+INTERNAL_FILES_URL=http://127.0.0.1:5001
+
# The time in seconds after the signature is rejected
FILES_ACCESS_TIMEOUT=300
diff --git a/api/configs/feature/__init__.py b/api/configs/feature/__init__.py
index c2eaa89b6e..4f5e5b4559 100644
--- a/api/configs/feature/__init__.py
+++ b/api/configs/feature/__init__.py
@@ -237,6 +237,13 @@ class FileAccessConfig(BaseSettings):
default="",
)
+ INTERNAL_FILES_URL: str = Field(
+ description="Internal base URL for file access within Docker network,"
+ " used for plugin daemon and internal service communication."
+ " Falls back to FILES_URL if not specified.",
+ default="",
+ )
+
FILES_ACCESS_TIMEOUT: int = Field(
description="Expiration time in seconds for file access URLs",
default=300,
diff --git a/api/controllers/console/app/mcp_server.py b/api/controllers/console/app/mcp_server.py
index 4f9e75c0d3..ccda97d80c 100644
--- a/api/controllers/console/app/mcp_server.py
+++ b/api/controllers/console/app/mcp_server.py
@@ -90,7 +90,11 @@ class AppMCPServerRefreshController(Resource):
def get(self, server_id):
if not current_user.is_editor:
raise NotFound()
- server = db.session.query(AppMCPServer).filter(AppMCPServer.id == server_id).first()
+ server = (
+ db.session.query(AppMCPServer)
+ .filter(AppMCPServer.id == server_id and AppMCPServer.tenant_id == current_user.current_tenant_id)
+ .first()
+ )
if not server:
raise NotFound()
server.server_code = AppMCPServer.generate_server_code(16)
diff --git a/api/controllers/console/workspace/tool_providers.py b/api/controllers/console/workspace/tool_providers.py
index 3c8f5e89ce..d8f04820ba 100644
--- a/api/controllers/console/workspace/tool_providers.py
+++ b/api/controllers/console/workspace/tool_providers.py
@@ -12,7 +12,11 @@ from werkzeug.exceptions import Forbidden
from configs import dify_config
from controllers.console import api
-from controllers.console.wraps import account_initialization_required, enterprise_license_required, setup_required
+from controllers.console.wraps import (
+ account_initialization_required,
+ enterprise_license_required,
+ setup_required,
+)
from core.mcp.auth.auth_flow import auth, handle_callback
from core.mcp.auth.auth_provider import OAuthClientProvider
from core.mcp.error import MCPAuthError, MCPError
diff --git a/api/core/file/helpers.py b/api/core/file/helpers.py
index 73fabdb11b..335ad2266a 100644
--- a/api/core/file/helpers.py
+++ b/api/core/file/helpers.py
@@ -21,7 +21,9 @@ def get_signed_file_url(upload_file_id: str) -> str:
def get_signed_file_url_for_plugin(filename: str, mimetype: str, tenant_id: str, user_id: str) -> str:
- url = f"{dify_config.FILES_URL}/files/upload/for-plugin"
+ # Plugin access should use internal URL for Docker network communication
+ base_url = dify_config.INTERNAL_FILES_URL or dify_config.FILES_URL
+ url = f"{base_url}/files/upload/for-plugin"
if user_id is None:
user_id = "DEFAULT-USER"
diff --git a/api/core/mcp/server/streamable_http.py b/api/core/mcp/server/streamable_http.py
index 6b422ae4ae..1c2cf570e2 100644
--- a/api/core/mcp/server/streamable_http.py
+++ b/api/core/mcp/server/streamable_http.py
@@ -89,6 +89,7 @@ class MCPServerStreamableHTTPRequestHandler:
types.ListToolsRequest: self.list_tools,
types.CallToolRequest: self.invoke_tool,
types.InitializedNotification: self.handle_notification,
+ types.PingRequest: self.handle_ping,
}
try:
if self.request_type in handle_map:
@@ -105,16 +106,19 @@ class MCPServerStreamableHTTPRequestHandler:
def handle_notification(self):
return "ping"
+ def handle_ping(self):
+ return types.EmptyResult()
+
def initialize(self):
request = cast(types.InitializeRequest, self.request.root)
client_info = request.params.clientInfo
- clinet_name = f"{client_info.name}@{client_info.version}"
+ client_name = f"{client_info.name}@{client_info.version}"
if not self.end_user:
end_user = EndUser(
tenant_id=self.app.tenant_id,
app_id=self.app.id,
type="mcp",
- name=clinet_name,
+ name=client_name,
session_id=generate_session_id(),
external_user_id=self.mcp_server.id,
)
diff --git a/api/core/model_runtime/entities/provider_entities.py b/api/core/model_runtime/entities/provider_entities.py
index d0f9ee13e5..c9aa8d1474 100644
--- a/api/core/model_runtime/entities/provider_entities.py
+++ b/api/core/model_runtime/entities/provider_entities.py
@@ -123,6 +123,8 @@ class ProviderEntity(BaseModel):
description: Optional[I18nObject] = None
icon_small: Optional[I18nObject] = None
icon_large: Optional[I18nObject] = None
+ icon_small_dark: Optional[I18nObject] = None
+ icon_large_dark: Optional[I18nObject] = None
background: Optional[str] = None
help: Optional[ProviderHelpEntity] = None
supported_model_types: Sequence[ModelType]
diff --git a/api/core/plugin/entities/plugin.py b/api/core/plugin/entities/plugin.py
index d6bbf05097..e5cf7ee03a 100644
--- a/api/core/plugin/entities/plugin.py
+++ b/api/core/plugin/entities/plugin.py
@@ -79,6 +79,7 @@ class PluginDeclaration(BaseModel):
name: str = Field(..., pattern=r"^[a-z0-9_-]{1,128}$")
description: I18nObject
icon: str
+ icon_dark: Optional[str] = Field(default=None)
label: I18nObject
category: PluginCategory
created_at: datetime.datetime
diff --git a/api/core/tools/custom_tool/provider.py b/api/core/tools/custom_tool/provider.py
index 3137d32013..fbe1d79137 100644
--- a/api/core/tools/custom_tool/provider.py
+++ b/api/core/tools/custom_tool/provider.py
@@ -39,19 +39,22 @@ class ApiToolProviderController(ToolProviderController):
type=ProviderConfig.Type.SELECT,
options=[
ProviderConfig.Option(value="none", label=I18nObject(en_US="None", zh_Hans="无")),
- ProviderConfig.Option(value="api_key", label=I18nObject(en_US="api_key", zh_Hans="api_key")),
+ ProviderConfig.Option(value="api_key_header", label=I18nObject(en_US="Header", zh_Hans="请求头")),
+ ProviderConfig.Option(
+ value="api_key_query", label=I18nObject(en_US="Query Param", zh_Hans="查询参数")
+ ),
],
default="none",
help=I18nObject(en_US="The auth type of the api provider", zh_Hans="api provider 的认证类型"),
)
]
- if auth_type == ApiProviderAuthType.API_KEY:
+ if auth_type == ApiProviderAuthType.API_KEY_HEADER:
credentials_schema = [
*credentials_schema,
ProviderConfig(
name="api_key_header",
required=False,
- default="api_key",
+ default="Authorization",
type=ProviderConfig.Type.TEXT_INPUT,
help=I18nObject(en_US="The header name of the api key", zh_Hans="携带 api key 的 header 名称"),
),
@@ -74,6 +77,25 @@ class ApiToolProviderController(ToolProviderController):
],
),
]
+ elif auth_type == ApiProviderAuthType.API_KEY_QUERY:
+ credentials_schema = [
+ *credentials_schema,
+ ProviderConfig(
+ name="api_key_query_param",
+ required=False,
+ default="key",
+ type=ProviderConfig.Type.TEXT_INPUT,
+ help=I18nObject(
+ en_US="The query parameter name of the api key", zh_Hans="携带 api key 的查询参数名称"
+ ),
+ ),
+ ProviderConfig(
+ name="api_key_value",
+ required=True,
+ type=ProviderConfig.Type.SECRET_INPUT,
+ help=I18nObject(en_US="The api key", zh_Hans="api key 的值"),
+ ),
+ ]
elif auth_type == ApiProviderAuthType.NONE:
pass
diff --git a/api/core/tools/custom_tool/tool.py b/api/core/tools/custom_tool/tool.py
index 2f5cc6d4c0..10653b9948 100644
--- a/api/core/tools/custom_tool/tool.py
+++ b/api/core/tools/custom_tool/tool.py
@@ -78,8 +78,8 @@ class ApiTool(Tool):
if "auth_type" not in credentials:
raise ToolProviderCredentialValidationError("Missing auth_type")
- if credentials["auth_type"] == "api_key":
- api_key_header = "api_key"
+ if credentials["auth_type"] in ("api_key_header", "api_key"): # backward compatibility:
+ api_key_header = "Authorization"
if "api_key_header" in credentials:
api_key_header = credentials["api_key_header"]
@@ -100,6 +100,11 @@ class ApiTool(Tool):
headers[api_key_header] = credentials["api_key_value"]
+ elif credentials["auth_type"] == "api_key_query":
+ # For query parameter authentication, we don't add anything to headers
+ # The query parameter will be added in do_http_request method
+ pass
+
needed_parameters = [parameter for parameter in (self.api_bundle.parameters or []) if parameter.required]
for parameter in needed_parameters:
if parameter.required and parameter.name not in parameters:
@@ -154,6 +159,15 @@ class ApiTool(Tool):
cookies = {}
files = []
+ # Add API key to query parameters if auth_type is api_key_query
+ if self.runtime and self.runtime.credentials:
+ credentials = self.runtime.credentials
+ if credentials.get("auth_type") == "api_key_query":
+ api_key_query_param = credentials.get("api_key_query_param", "key")
+ api_key_value = credentials.get("api_key_value")
+ if api_key_value:
+ params[api_key_query_param] = api_key_value
+
# check parameters
for parameter in self.api_bundle.openapi.get("parameters", []):
value = self.get_parameter_value(parameter, parameters)
@@ -213,7 +227,8 @@ class ApiTool(Tool):
elif "default" in property:
body[name] = property["default"]
else:
- body[name] = None
+ # omit optional parameters that weren't provided, instead of setting them to None
+ pass
break
# replace path parameters
diff --git a/api/core/tools/entities/api_entities.py b/api/core/tools/entities/api_entities.py
index 48e6e86144..27ce96b90e 100644
--- a/api/core/tools/entities/api_entities.py
+++ b/api/core/tools/entities/api_entities.py
@@ -28,6 +28,7 @@ class ToolProviderApiEntity(BaseModel):
name: str # identifier
description: I18nObject
icon: str | dict
+ icon_dark: Optional[str | dict] = Field(default=None, description="The dark icon of the tool")
label: I18nObject # label
type: ToolProviderType
masked_credentials: Optional[dict] = None
@@ -72,6 +73,7 @@ class ToolProviderApiEntity(BaseModel):
"plugin_unique_identifier": self.plugin_unique_identifier,
"description": self.description.to_dict(),
"icon": self.icon,
+ "icon_dark": self.icon_dark,
"label": self.label.to_dict(),
"type": self.type.value,
"team_credentials": self.masked_credentials,
diff --git a/api/core/tools/entities/tool_entities.py b/api/core/tools/entities/tool_entities.py
index 7bf4713167..2e53f1ae73 100644
--- a/api/core/tools/entities/tool_entities.py
+++ b/api/core/tools/entities/tool_entities.py
@@ -96,7 +96,8 @@ class ApiProviderAuthType(Enum):
"""
NONE = "none"
- API_KEY = "api_key"
+ API_KEY_HEADER = "api_key_header"
+ API_KEY_QUERY = "api_key_query"
@classmethod
def value_of(cls, value: str) -> "ApiProviderAuthType":
@@ -317,6 +318,7 @@ class ToolProviderIdentity(BaseModel):
name: str = Field(..., description="The name of the tool")
description: I18nObject = Field(..., description="The description of the tool")
icon: str = Field(..., description="The icon of the tool")
+ icon_dark: Optional[str] = Field(default=None, description="The dark icon of the tool")
label: I18nObject = Field(..., description="The label of the tool")
tags: Optional[list[ToolLabelEnum]] = Field(
default=[],
diff --git a/api/core/tools/signature.py b/api/core/tools/signature.py
index e80005d7bf..5cdf473542 100644
--- a/api/core/tools/signature.py
+++ b/api/core/tools/signature.py
@@ -9,9 +9,10 @@ from configs import dify_config
def sign_tool_file(tool_file_id: str, extension: str) -> str:
"""
- sign file to get a temporary url
+ sign file to get a temporary url for plugin access
"""
- base_url = dify_config.FILES_URL
+ # Use internal URL for plugin/tool file access in Docker environments
+ base_url = dify_config.INTERNAL_FILES_URL or dify_config.FILES_URL
file_preview_url = f"{base_url}/files/tools/{tool_file_id}{extension}"
timestamp = str(int(time.time()))
diff --git a/api/core/tools/tool_file_manager.py b/api/core/tools/tool_file_manager.py
index b849f51064..ece02f9d59 100644
--- a/api/core/tools/tool_file_manager.py
+++ b/api/core/tools/tool_file_manager.py
@@ -35,9 +35,10 @@ class ToolFileManager:
@staticmethod
def sign_file(tool_file_id: str, extension: str) -> str:
"""
- sign file to get a temporary url
+ sign file to get a temporary url for plugin access
"""
- base_url = dify_config.FILES_URL
+ # Use internal URL for plugin/tool file access in Docker environments
+ base_url = dify_config.INTERNAL_FILES_URL or dify_config.FILES_URL
file_preview_url = f"{base_url}/files/tools/{tool_file_id}{extension}"
timestamp = str(int(time.time()))
diff --git a/api/core/tools/tool_manager.py b/api/core/tools/tool_manager.py
index 4bafd506b9..9d5000f8d4 100644
--- a/api/core/tools/tool_manager.py
+++ b/api/core/tools/tool_manager.py
@@ -692,9 +692,16 @@ class ToolManager:
if provider is None:
raise ToolProviderNotFoundError(f"api provider {provider_id} not found")
+ auth_type = ApiProviderAuthType.NONE
+ provider_auth_type = provider.credentials.get("auth_type")
+ if provider_auth_type in ("api_key_header", "api_key"): # backward compatibility
+ auth_type = ApiProviderAuthType.API_KEY_HEADER
+ elif provider_auth_type == "api_key_query":
+ auth_type = ApiProviderAuthType.API_KEY_QUERY
+
controller = ApiToolProviderController.from_db(
provider,
- ApiProviderAuthType.API_KEY if provider.credentials["auth_type"] == "api_key" else ApiProviderAuthType.NONE,
+ auth_type,
)
controller.load_bundled_tools(provider.tools)
@@ -753,9 +760,16 @@ class ToolManager:
credentials = {}
# package tool provider controller
+ auth_type = ApiProviderAuthType.NONE
+ credentials_auth_type = credentials.get("auth_type")
+ if credentials_auth_type in ("api_key_header", "api_key"): # backward compatibility
+ auth_type = ApiProviderAuthType.API_KEY_HEADER
+ elif credentials_auth_type == "api_key_query":
+ auth_type = ApiProviderAuthType.API_KEY_QUERY
+
controller = ApiToolProviderController.from_db(
provider_obj,
- ApiProviderAuthType.API_KEY if credentials["auth_type"] == "api_key" else ApiProviderAuthType.NONE,
+ auth_type,
)
# init tool configuration
encrypter, _ = create_tool_provider_encrypter(
diff --git a/api/core/workflow/nodes/tool/tool_node.py b/api/core/workflow/nodes/tool/tool_node.py
index f5898dd605..48627a229d 100644
--- a/api/core/workflow/nodes/tool/tool_node.py
+++ b/api/core/workflow/nodes/tool/tool_node.py
@@ -329,6 +329,7 @@ class ToolNode(BaseNode[ToolNodeData]):
icon = current_plugin.declaration.icon
except StopIteration:
pass
+ icon_dark = None
try:
builtin_tool = next(
provider
@@ -339,10 +340,12 @@ class ToolNode(BaseNode[ToolNodeData]):
if provider.name == dict_metadata["provider"]
)
icon = builtin_tool.icon
+ icon_dark = builtin_tool.icon_dark
except StopIteration:
pass
dict_metadata["icon"] = icon
+ dict_metadata["icon_dark"] = icon_dark
message.message.metadata = dict_metadata
agent_log = AgentLogEvent(
id=message.message.id,
diff --git a/api/pyproject.toml b/api/pyproject.toml
index 9f2e3ed331..420bc771b6 100644
--- a/api/pyproject.toml
+++ b/api/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "dify-api"
-version = "1.5.1"
+version = "1.6.0"
requires-python = ">=3.11,<3.13"
dependencies = [
diff --git a/api/services/tools/mcp_tools_mange_service.py b/api/services/tools/mcp_tools_mange_service.py
index fda6da5983..3d3ecdcced 100644
--- a/api/services/tools/mcp_tools_mange_service.py
+++ b/api/services/tools/mcp_tools_mange_service.py
@@ -70,7 +70,6 @@ class MCPToolManageService:
MCPToolProvider.server_url_hash == server_url_hash,
MCPToolProvider.server_identifier == server_identifier,
),
- MCPToolProvider.tenant_id == tenant_id,
)
.first()
)
diff --git a/api/services/tools/tools_transform_service.py b/api/services/tools/tools_transform_service.py
index f54cec74f0..36b892e205 100644
--- a/api/services/tools/tools_transform_service.py
+++ b/api/services/tools/tools_transform_service.py
@@ -77,10 +77,18 @@ class ToolTransformService:
provider.icon = ToolTransformService.get_plugin_icon_url(
tenant_id=tenant_id, filename=provider.icon
)
+ if isinstance(provider.icon_dark, str) and provider.icon_dark:
+ provider.icon_dark = ToolTransformService.get_plugin_icon_url(
+ tenant_id=tenant_id, filename=provider.icon_dark
+ )
else:
provider.icon = ToolTransformService.get_tool_provider_icon_url(
provider_type=provider.type.value, provider_name=provider.name, icon=provider.icon
)
+ if provider.icon_dark:
+ provider.icon_dark = ToolTransformService.get_tool_provider_icon_url(
+ provider_type=provider.type.value, provider_name=provider.name, icon=provider.icon_dark
+ )
@classmethod
def builtin_provider_to_user_provider(
@@ -98,6 +106,7 @@ class ToolTransformService:
name=provider_controller.entity.identity.name,
description=provider_controller.entity.identity.description,
icon=provider_controller.entity.identity.icon,
+ icon_dark=provider_controller.entity.identity.icon_dark,
label=provider_controller.entity.identity.label,
type=ToolProviderType.BUILT_IN,
masked_credentials={},
@@ -165,11 +174,16 @@ class ToolTransformService:
convert provider controller to user provider
"""
# package tool provider controller
+ auth_type = ApiProviderAuthType.NONE
+ credentials_auth_type = db_provider.credentials.get("auth_type")
+ if credentials_auth_type in ("api_key_header", "api_key"): # backward compatibility
+ auth_type = ApiProviderAuthType.API_KEY_HEADER
+ elif credentials_auth_type == "api_key_query":
+ auth_type = ApiProviderAuthType.API_KEY_QUERY
+
controller = ApiToolProviderController.from_db(
db_provider=db_provider,
- auth_type=ApiProviderAuthType.API_KEY
- if db_provider.credentials["auth_type"] == "api_key"
- else ApiProviderAuthType.NONE,
+ auth_type=auth_type,
)
return controller
@@ -194,6 +208,7 @@ class ToolTransformService:
name=provider_controller.entity.identity.name,
description=provider_controller.entity.identity.description,
icon=provider_controller.entity.identity.icon,
+ icon_dark=provider_controller.entity.identity.icon_dark,
label=provider_controller.entity.identity.label,
type=ToolProviderType.WORKFLOW,
masked_credentials={},
diff --git a/api/uv.lock b/api/uv.lock
index 45831e24a1..e108e0c445 100644
--- a/api/uv.lock
+++ b/api/uv.lock
@@ -1217,7 +1217,7 @@ wheels = [
[[package]]
name = "dify-api"
-version = "1.5.1"
+version = "1.6.0"
source = { virtual = "." }
dependencies = [
{ name = "arize-phoenix-otel" },
diff --git a/docker/.env.example b/docker/.env.example
index 5eefedcb5a..1edc0294ba 100644
--- a/docker/.env.example
+++ b/docker/.env.example
@@ -47,6 +47,11 @@ APP_WEB_URL=
# ensuring port 5001 is externally accessible (see docker-compose.yaml).
FILES_URL=
+# INTERNAL_FILES_URL is used for plugin daemon communication within Docker network.
+# Set this to the internal Docker service URL for proper plugin file access.
+# Example: INTERNAL_FILES_URL=http://api:5001
+INTERNAL_FILES_URL=
+
# ------------------------------
# Server Configuration
# ------------------------------
diff --git a/docker/docker-compose-template.yaml b/docker/docker-compose-template.yaml
index 954ba16be1..003038c539 100644
--- a/docker/docker-compose-template.yaml
+++ b/docker/docker-compose-template.yaml
@@ -2,7 +2,7 @@ x-shared-env: &shared-api-worker-env
services:
# API service
api:
- image: langgenius/dify-api:1.5.1
+ image: langgenius/dify-api:1.6.0
restart: always
environment:
# Use the shared environment variables.
@@ -31,7 +31,7 @@ services:
# worker service
# The Celery worker for processing the queue.
worker:
- image: langgenius/dify-api:1.5.1
+ image: langgenius/dify-api:1.6.0
restart: always
environment:
# Use the shared environment variables.
@@ -79,7 +79,7 @@ services:
# Frontend web application.
web:
- image: langgenius/dify-web:1.5.1
+ image: langgenius/dify-web:1.6.0
restart: always
environment:
CONSOLE_API_URL: ${CONSOLE_API_URL:-}
diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml
index df495bfa7f..dd54869410 100644
--- a/docker/docker-compose.yaml
+++ b/docker/docker-compose.yaml
@@ -11,6 +11,7 @@ x-shared-env: &shared-api-worker-env
APP_API_URL: ${APP_API_URL:-}
APP_WEB_URL: ${APP_WEB_URL:-}
FILES_URL: ${FILES_URL:-}
+ INTERNAL_FILES_URL: ${INTERNAL_FILES_URL:-}
LOG_LEVEL: ${LOG_LEVEL:-INFO}
LOG_FILE: ${LOG_FILE:-/app/logs/server.log}
LOG_FILE_MAX_SIZE: ${LOG_FILE_MAX_SIZE:-20}
@@ -526,7 +527,7 @@ x-shared-env: &shared-api-worker-env
services:
# API service
api:
- image: langgenius/dify-api:1.5.1
+ image: langgenius/dify-api:1.6.0
restart: always
environment:
# Use the shared environment variables.
@@ -555,7 +556,7 @@ services:
# worker service
# The Celery worker for processing the queue.
worker:
- image: langgenius/dify-api:1.5.1
+ image: langgenius/dify-api:1.6.0
restart: always
environment:
# Use the shared environment variables.
@@ -603,7 +604,7 @@ services:
# Frontend web application.
web:
- image: langgenius/dify-web:1.5.1
+ image: langgenius/dify-web:1.6.0
restart: always
environment:
CONSOLE_API_URL: ${CONSOLE_API_URL:-}
diff --git a/web/app/components/datasets/create/index.tsx b/web/app/components/datasets/create/index.tsx
index b1e4087226..a1ff2f5d87 100644
--- a/web/app/components/datasets/create/index.tsx
+++ b/web/app/components/datasets/create/index.tsx
@@ -122,7 +122,7 @@ const DatasetUpdateForm = ({ datasetId }: DatasetUpdateFormProps) => {
return