Skip to content

Commit ec5cdbc

Browse files
authored
Merge branch 'langgenius:main' into main
2 parents 744d7c9 + 016ff0f commit ec5cdbc

391 files changed

Lines changed: 17702 additions & 2064 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

api/.env.example

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@ APP_WEB_URL=http://127.0.0.1:3000
1717
# Files URL
1818
FILES_URL=http://127.0.0.1:5001
1919

20+
# INTERNAL_FILES_URL is used for plugin daemon communication within Docker network.
21+
# Set this to the internal Docker service URL for proper plugin file access.
22+
# Example: INTERNAL_FILES_URL=http://api:5001
23+
INTERNAL_FILES_URL=http://127.0.0.1:5001
24+
2025
# The time in seconds after the signature is rejected
2126
FILES_ACCESS_TIMEOUT=300
2227

api/configs/feature/__init__.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,13 @@ class FileAccessConfig(BaseSettings):
237237
default="",
238238
)
239239

240+
INTERNAL_FILES_URL: str = Field(
241+
description="Internal base URL for file access within Docker network,"
242+
" used for plugin daemon and internal service communication."
243+
" Falls back to FILES_URL if not specified.",
244+
default="",
245+
)
246+
240247
FILES_ACCESS_TIMEOUT: int = Field(
241248
description="Expiration time in seconds for file access URLs",
242249
default=300,

api/controllers/console/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
conversation,
5757
conversation_variables,
5858
generator,
59+
mcp_server,
5960
message,
6061
model_config,
6162
ops_trace,
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import json
2+
from enum import StrEnum
3+
4+
from flask_login import current_user
5+
from flask_restful import Resource, marshal_with, reqparse
6+
from werkzeug.exceptions import NotFound
7+
8+
from controllers.console import api
9+
from controllers.console.app.wraps import get_app_model
10+
from controllers.console.wraps import account_initialization_required, setup_required
11+
from extensions.ext_database import db
12+
from fields.app_fields import app_server_fields
13+
from libs.login import login_required
14+
from models.model import AppMCPServer
15+
16+
17+
class AppMCPServerStatus(StrEnum):
18+
ACTIVE = "active"
19+
INACTIVE = "inactive"
20+
21+
22+
class AppMCPServerController(Resource):
23+
@setup_required
24+
@login_required
25+
@account_initialization_required
26+
@get_app_model
27+
@marshal_with(app_server_fields)
28+
def get(self, app_model):
29+
server = db.session.query(AppMCPServer).filter(AppMCPServer.app_id == app_model.id).first()
30+
return server
31+
32+
@setup_required
33+
@login_required
34+
@account_initialization_required
35+
@get_app_model
36+
@marshal_with(app_server_fields)
37+
def post(self, app_model):
38+
# The role of the current user in the ta table must be editor, admin, or owner
39+
if not current_user.is_editor:
40+
raise NotFound()
41+
parser = reqparse.RequestParser()
42+
parser.add_argument("description", type=str, required=True, location="json")
43+
parser.add_argument("parameters", type=dict, required=True, location="json")
44+
args = parser.parse_args()
45+
server = AppMCPServer(
46+
name=app_model.name,
47+
description=args["description"],
48+
parameters=json.dumps(args["parameters"], ensure_ascii=False),
49+
status=AppMCPServerStatus.ACTIVE,
50+
app_id=app_model.id,
51+
tenant_id=current_user.current_tenant_id,
52+
server_code=AppMCPServer.generate_server_code(16),
53+
)
54+
db.session.add(server)
55+
db.session.commit()
56+
return server
57+
58+
@setup_required
59+
@login_required
60+
@account_initialization_required
61+
@get_app_model
62+
@marshal_with(app_server_fields)
63+
def put(self, app_model):
64+
if not current_user.is_editor:
65+
raise NotFound()
66+
parser = reqparse.RequestParser()
67+
parser.add_argument("id", type=str, required=True, location="json")
68+
parser.add_argument("description", type=str, required=True, location="json")
69+
parser.add_argument("parameters", type=dict, required=True, location="json")
70+
parser.add_argument("status", type=str, required=False, location="json")
71+
args = parser.parse_args()
72+
server = db.session.query(AppMCPServer).filter(AppMCPServer.id == args["id"]).first()
73+
if not server:
74+
raise NotFound()
75+
server.description = args["description"]
76+
server.parameters = json.dumps(args["parameters"], ensure_ascii=False)
77+
if args["status"]:
78+
if args["status"] not in [status.value for status in AppMCPServerStatus]:
79+
raise ValueError("Invalid status")
80+
server.status = args["status"]
81+
db.session.commit()
82+
return server
83+
84+
85+
class AppMCPServerRefreshController(Resource):
86+
@setup_required
87+
@login_required
88+
@account_initialization_required
89+
@marshal_with(app_server_fields)
90+
def get(self, server_id):
91+
if not current_user.is_editor:
92+
raise NotFound()
93+
server = (
94+
db.session.query(AppMCPServer)
95+
.filter(AppMCPServer.id == server_id)
96+
.filter(AppMCPServer.tenant_id == current_user.current_tenant_id)
97+
.first()
98+
)
99+
if not server:
100+
raise NotFound()
101+
server.server_code = AppMCPServer.generate_server_code(16)
102+
db.session.commit()
103+
return server
104+
105+
106+
api.add_resource(AppMCPServerController, "/apps/<uuid:app_id>/server")
107+
api.add_resource(AppMCPServerRefreshController, "/apps/<uuid:server_id>/server/refresh")

api/controllers/console/workspace/tool_providers.py

Lines changed: 188 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import io
2+
from urllib.parse import urlparse
23

3-
from flask import send_file
4+
from flask import redirect, send_file
45
from flask_login import current_user
56
from flask_restful import Resource, reqparse
67
from sqlalchemy.orm import Session
@@ -9,17 +10,34 @@
910
from configs import dify_config
1011
from controllers.console import api
1112
from controllers.console.wraps import account_initialization_required, enterprise_license_required, setup_required
13+
from core.mcp.auth.auth_flow import auth, handle_callback
14+
from core.mcp.auth.auth_provider import OAuthClientProvider
15+
from core.mcp.error import MCPAuthError, MCPError
16+
from core.mcp.mcp_client import MCPClient
1217
from core.model_runtime.utils.encoders import jsonable_encoder
1318
from extensions.ext_database import db
1419
from libs.helper import alphanumeric, uuid_value
1520
from libs.login import login_required
1621
from services.tools.api_tools_manage_service import ApiToolManageService
1722
from services.tools.builtin_tools_manage_service import BuiltinToolManageService
23+
from services.tools.mcp_tools_mange_service import MCPToolManageService
1824
from services.tools.tool_labels_service import ToolLabelsService
1925
from services.tools.tools_manage_service import ToolCommonService
26+
from services.tools.tools_transform_service import ToolTransformService
2027
from services.tools.workflow_tools_manage_service import WorkflowToolManageService
2128

2229

30+
def is_valid_url(url: str) -> bool:
31+
if not url:
32+
return False
33+
34+
try:
35+
parsed = urlparse(url)
36+
return all([parsed.scheme, parsed.netloc]) and parsed.scheme in ["http", "https"]
37+
except Exception:
38+
return False
39+
40+
2341
class ToolProviderListApi(Resource):
2442
@setup_required
2543
@login_required
@@ -34,7 +52,7 @@ def get(self):
3452
req.add_argument(
3553
"type",
3654
type=str,
37-
choices=["builtin", "model", "api", "workflow"],
55+
choices=["builtin", "model", "api", "workflow", "mcp"],
3856
required=False,
3957
nullable=True,
4058
location="args",
@@ -613,6 +631,166 @@ def get(self):
613631
return jsonable_encoder(ToolLabelsService.list_tool_labels())
614632

615633

634+
class ToolProviderMCPApi(Resource):
635+
@setup_required
636+
@login_required
637+
@account_initialization_required
638+
def post(self):
639+
parser = reqparse.RequestParser()
640+
parser.add_argument("server_url", type=str, required=True, nullable=False, location="json")
641+
parser.add_argument("name", type=str, required=True, nullable=False, location="json")
642+
parser.add_argument("icon", type=str, required=True, nullable=False, location="json")
643+
parser.add_argument("icon_type", type=str, required=True, nullable=False, location="json")
644+
parser.add_argument("icon_background", type=str, required=False, nullable=True, location="json", default="")
645+
parser.add_argument("server_identifier", type=str, required=True, nullable=False, location="json")
646+
args = parser.parse_args()
647+
user = current_user
648+
if not is_valid_url(args["server_url"]):
649+
raise ValueError("Server URL is not valid.")
650+
return jsonable_encoder(
651+
MCPToolManageService.create_mcp_provider(
652+
tenant_id=user.current_tenant_id,
653+
server_url=args["server_url"],
654+
name=args["name"],
655+
icon=args["icon"],
656+
icon_type=args["icon_type"],
657+
icon_background=args["icon_background"],
658+
user_id=user.id,
659+
server_identifier=args["server_identifier"],
660+
)
661+
)
662+
663+
@setup_required
664+
@login_required
665+
@account_initialization_required
666+
def put(self):
667+
parser = reqparse.RequestParser()
668+
parser.add_argument("server_url", type=str, required=True, nullable=False, location="json")
669+
parser.add_argument("name", type=str, required=True, nullable=False, location="json")
670+
parser.add_argument("icon", type=str, required=True, nullable=False, location="json")
671+
parser.add_argument("icon_type", type=str, required=True, nullable=False, location="json")
672+
parser.add_argument("icon_background", type=str, required=False, nullable=True, location="json")
673+
parser.add_argument("provider_id", type=str, required=True, nullable=False, location="json")
674+
parser.add_argument("server_identifier", type=str, required=True, nullable=False, location="json")
675+
args = parser.parse_args()
676+
if not is_valid_url(args["server_url"]):
677+
if "[__HIDDEN__]" in args["server_url"]:
678+
pass
679+
else:
680+
raise ValueError("Server URL is not valid.")
681+
MCPToolManageService.update_mcp_provider(
682+
tenant_id=current_user.current_tenant_id,
683+
provider_id=args["provider_id"],
684+
server_url=args["server_url"],
685+
name=args["name"],
686+
icon=args["icon"],
687+
icon_type=args["icon_type"],
688+
icon_background=args["icon_background"],
689+
server_identifier=args["server_identifier"],
690+
)
691+
return {"result": "success"}
692+
693+
@setup_required
694+
@login_required
695+
@account_initialization_required
696+
def delete(self):
697+
parser = reqparse.RequestParser()
698+
parser.add_argument("provider_id", type=str, required=True, nullable=False, location="json")
699+
args = parser.parse_args()
700+
MCPToolManageService.delete_mcp_tool(tenant_id=current_user.current_tenant_id, provider_id=args["provider_id"])
701+
return {"result": "success"}
702+
703+
704+
class ToolMCPAuthApi(Resource):
705+
@setup_required
706+
@login_required
707+
@account_initialization_required
708+
def post(self):
709+
parser = reqparse.RequestParser()
710+
parser.add_argument("provider_id", type=str, required=True, nullable=False, location="json")
711+
parser.add_argument("authorization_code", type=str, required=False, nullable=True, location="json")
712+
args = parser.parse_args()
713+
provider_id = args["provider_id"]
714+
tenant_id = current_user.current_tenant_id
715+
provider = MCPToolManageService.get_mcp_provider_by_provider_id(provider_id, tenant_id)
716+
if not provider:
717+
raise ValueError("provider not found")
718+
try:
719+
with MCPClient(
720+
provider.decrypted_server_url,
721+
provider_id,
722+
tenant_id,
723+
authed=False,
724+
authorization_code=args["authorization_code"],
725+
for_list=True,
726+
):
727+
MCPToolManageService.update_mcp_provider_credentials(
728+
mcp_provider=provider,
729+
credentials=provider.decrypted_credentials,
730+
authed=True,
731+
)
732+
return {"result": "success"}
733+
734+
except MCPAuthError:
735+
auth_provider = OAuthClientProvider(provider_id, tenant_id, for_list=True)
736+
return auth(auth_provider, provider.decrypted_server_url, args["authorization_code"])
737+
except MCPError as e:
738+
MCPToolManageService.update_mcp_provider_credentials(
739+
mcp_provider=provider,
740+
credentials={},
741+
authed=False,
742+
)
743+
raise ValueError(f"Failed to connect to MCP server: {e}") from e
744+
745+
746+
class ToolMCPDetailApi(Resource):
747+
@setup_required
748+
@login_required
749+
@account_initialization_required
750+
def get(self, provider_id):
751+
user = current_user
752+
provider = MCPToolManageService.get_mcp_provider_by_provider_id(provider_id, user.current_tenant_id)
753+
return jsonable_encoder(ToolTransformService.mcp_provider_to_user_provider(provider, for_list=True))
754+
755+
756+
class ToolMCPListAllApi(Resource):
757+
@setup_required
758+
@login_required
759+
@account_initialization_required
760+
def get(self):
761+
user = current_user
762+
tenant_id = user.current_tenant_id
763+
764+
tools = MCPToolManageService.retrieve_mcp_tools(tenant_id=tenant_id)
765+
766+
return [tool.to_dict() for tool in tools]
767+
768+
769+
class ToolMCPUpdateApi(Resource):
770+
@setup_required
771+
@login_required
772+
@account_initialization_required
773+
def get(self, provider_id):
774+
tenant_id = current_user.current_tenant_id
775+
tools = MCPToolManageService.list_mcp_tool_from_remote_server(
776+
tenant_id=tenant_id,
777+
provider_id=provider_id,
778+
)
779+
return jsonable_encoder(tools)
780+
781+
782+
class ToolMCPCallbackApi(Resource):
783+
def get(self):
784+
parser = reqparse.RequestParser()
785+
parser.add_argument("code", type=str, required=True, nullable=False, location="args")
786+
parser.add_argument("state", type=str, required=True, nullable=False, location="args")
787+
args = parser.parse_args()
788+
state_key = args["state"]
789+
authorization_code = args["code"]
790+
handle_callback(state_key, authorization_code)
791+
return redirect(f"{dify_config.CONSOLE_WEB_URL}/oauth-callback")
792+
793+
616794
# tool provider
617795
api.add_resource(ToolProviderListApi, "/workspaces/current/tool-providers")
618796

@@ -647,8 +825,15 @@ def get(self):
647825
api.add_resource(ToolWorkflowProviderGetApi, "/workspaces/current/tool-provider/workflow/get")
648826
api.add_resource(ToolWorkflowProviderListToolApi, "/workspaces/current/tool-provider/workflow/tools")
649827

828+
# mcp tool provider
829+
api.add_resource(ToolMCPDetailApi, "/workspaces/current/tool-provider/mcp/tools/<path:provider_id>")
830+
api.add_resource(ToolProviderMCPApi, "/workspaces/current/tool-provider/mcp")
831+
api.add_resource(ToolMCPUpdateApi, "/workspaces/current/tool-provider/mcp/update/<path:provider_id>")
832+
api.add_resource(ToolMCPAuthApi, "/workspaces/current/tool-provider/mcp/auth")
833+
api.add_resource(ToolMCPCallbackApi, "/mcp/oauth/callback")
834+
650835
api.add_resource(ToolBuiltinListApi, "/workspaces/current/tools/builtin")
651836
api.add_resource(ToolApiListApi, "/workspaces/current/tools/api")
837+
api.add_resource(ToolMCPListAllApi, "/workspaces/current/tools/mcp")
652838
api.add_resource(ToolWorkflowListApi, "/workspaces/current/tools/workflow")
653-
654839
api.add_resource(ToolLabelsApi, "/workspaces/current/tool-labels")

api/controllers/files/upload.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,5 @@ def post(self):
8787
except services.errors.file.UnsupportedFileTypeError:
8888
raise UnsupportedFileTypeError()
8989

90-
return tool_file, 201
91-
9290

9391
api.add_resource(PluginUploadFileApi, "/files/upload/for-plugin")

api/controllers/mcp/__init__.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
from flask import Blueprint
2+
3+
from libs.external_api import ExternalApi
4+
5+
bp = Blueprint("mcp", __name__, url_prefix="/mcp")
6+
api = ExternalApi(bp)
7+
8+
from . import mcp

0 commit comments

Comments
 (0)