Merge branch 'feat/mcp' into deploy/dev

This commit is contained in:
Novice 2025-05-30 18:26:35 +08:00
commit 657acdcfb9
6 changed files with 101 additions and 67 deletions

View File

@ -0,0 +1,8 @@
from flask import Blueprint
from libs.external_api import ExternalApi
bp = Blueprint("mcp", __name__, url_prefix="/mcp")
api = ExternalApi(bp)
from . import mcp

View File

@ -0,0 +1,66 @@
from amqp import NotFound
from flask_restful import Resource, reqparse
from pydantic import ValidationError
from controllers.mcp import api
from controllers.web.error import (
AppUnavailableError,
)
from core.app.app_config.entities import VariableEntity
from core.mcp.server.handler import MCPServerReuqestHandler
from core.mcp.types import ClientRequest
from extensions.ext_database import db
from libs import helper
from models.model import App, AppMCPServer, AppMode
class MCPAppApi(Resource):
def post(self, server_code):
def int_or_str(value):
if isinstance(value, int):
return value
elif isinstance(value, str):
return int(value)
else:
raise ValueError("Invalid id")
parser = reqparse.RequestParser()
parser.add_argument("jsonrpc", type=str, required=True, location="json")
parser.add_argument("method", type=str, required=True, location="json")
parser.add_argument("params", type=dict, required=True, location="json")
parser.add_argument("id", type=int_or_str, required=True, location="json")
args = parser.parse_args()
server = db.session.query(AppMCPServer).filter(AppMCPServer.server_code == server_code).first()
if not server:
raise NotFound("Server Not Found")
app = db.session.query(App).filter(App.id == server.app_id).first()
if not app:
raise NotFound("App Not Found")
if app.mode in {AppMode.ADVANCED_CHAT.value, AppMode.WORKFLOW.value}:
workflow = app.workflow
if workflow is None:
raise AppUnavailableError()
features_dict = workflow.features_dict
user_input_form = workflow.user_input_form(to_old_structure=True)
else:
app_model_config = app.app_model_config
if app_model_config is None:
raise AppUnavailableError()
features_dict = app_model_config.to_dict()
user_input_form = features_dict.get("user_input_form", [])
try:
user_input_form = [VariableEntity.model_validate(list(item.values())[0]) for item in user_input_form]
except ValidationError as e:
raise ValueError(f"Invalid user_input_form: {str(e)}")
try:
request = ClientRequest.model_validate(args)
except ValidationError as e:
raise ValueError(f"Invalid MCP request: {str(e)}")
mcp_server_handler = MCPServerReuqestHandler(app, request, user_input_form)
return helper.compact_generate_response(mcp_server_handler.handle())
api.add_resource(MCPAppApi, "/server/<string:server_code>/mcp")

View File

@ -1,7 +1,6 @@
import logging
from flask_restful import Resource, reqparse
from pydantic import ValidationError
from flask_restful import reqparse
from werkzeug.exceptions import InternalServerError, NotFound
import services
@ -18,7 +17,6 @@ from controllers.web.error import (
)
from controllers.web.error import InvokeRateLimitError as InvokeRateLimitHttpError
from controllers.web.wraps import WebApiResource
from core.app.app_config.entities import VariableEntity
from core.app.apps.base_app_queue_manager import AppQueueManager
from core.app.entities.app_invoke_entities import InvokeFrom
from core.errors.error import (
@ -26,13 +24,10 @@ from core.errors.error import (
ProviderTokenNotInitError,
QuotaExceededError,
)
from core.mcp.server.handler import MCPServerReuqestHandler
from core.mcp.types import ClientRequest
from core.model_runtime.errors.invoke import InvokeError
from extensions.ext_database import db
from libs import helper
from libs.helper import uuid_value
from models.model import App, AppMCPServer, AppMode
from models.model import AppMode
from services.app_generate_service import AppGenerateService
from services.errors.llm import InvokeRateLimitError
@ -154,57 +149,7 @@ class ChatStopApi(WebApiResource):
return {"result": "success"}, 200
class ChatMCPApi(Resource):
def post(self, server_code):
def int_or_str(value):
if isinstance(value, int):
return value
elif isinstance(value, str):
return int(value)
else:
raise ValueError("Invalid id")
parser = reqparse.RequestParser()
parser.add_argument("jsonrpc", type=str, required=True, location="json")
parser.add_argument("method", type=str, required=True, location="json")
parser.add_argument("params", type=dict, required=True, location="json")
parser.add_argument("id", type=int_or_str, required=True, location="json")
args = parser.parse_args()
server = db.session.query(AppMCPServer).filter(AppMCPServer.server_code == server_code).first()
if not server:
raise NotFound("Server Not Found")
app = db.session.query(App).filter(App.id == server.app_id).first()
if not app:
raise NotFound("App Not Found")
if app.mode in {AppMode.ADVANCED_CHAT.value, AppMode.WORKFLOW.value}:
workflow = app.workflow
if workflow is None:
raise AppUnavailableError()
features_dict = workflow.features_dict
user_input_form = workflow.user_input_form(to_old_structure=True)
else:
app_model_config = app.app_model_config
if app_model_config is None:
raise AppUnavailableError()
features_dict = app_model_config.to_dict()
user_input_form = features_dict.get("user_input_form", [])
try:
user_input_form = [VariableEntity.model_validate(item) for item in user_input_form]
except ValidationError as e:
raise ValueError(f"Invalid user_input_form: {str(e)}")
try:
request = ClientRequest.model_validate(args)
except ValidationError as e:
raise ValueError(f"Invalid MCP request: {str(e)}")
mcp_server_handler = MCPServerReuqestHandler(app, request, user_input_form)
return helper.compact_generate_response(mcp_server_handler.handle())
api.add_resource(CompletionApi, "/completion-messages")
api.add_resource(CompletionStopApi, "/completion-messages/<string:task_id>/stop")
api.add_resource(ChatApi, "/chat-messages")
api.add_resource(ChatMCPApi, "/server/<string:server_code>/mcp")
api.add_resource(ChatStopApi, "/chat-messages/<string:task_id>/stop")

View File

@ -22,9 +22,9 @@ class MCPServerReuqestHandler:
def __init__(self, app: App, request: types.ClientRequest, user_input_form: list[VariableEntity]):
self.app = app
self.request = request
self.mcp_server: AppMCPServer = self.app.mcp_server
if not self.mcp_server:
if not self.app.mcp_server:
raise ValueError("MCP server not found")
self.mcp_server: AppMCPServer = self.app.mcp_server
self.end_user = self.retrieve_end_user()
self.user_input_form = user_input_form
@ -47,13 +47,9 @@ class MCPServerReuqestHandler:
"required": required,
},
},
"required": "query",
"required": ["query", "inputs"],
}
@property
def output_parameters(self):
return self.app.output_schema
@property
def capabilities(self):
return types.ServerCapabilities(
@ -160,6 +156,7 @@ class MCPServerReuqestHandler:
parameters = {}
required = []
for item in user_input_form:
parameters[item.variable] = {}
if item.type in (
VariableEntityType.FILE,
VariableEntityType.FILE_LIST,
@ -168,12 +165,13 @@ class MCPServerReuqestHandler:
continue
if item.required:
required.append(item.variable)
parameters[item.variable]["description"] = self.mcp_server.parameters_dict[item.label]["description"]
description = self.mcp_server.parameters_dict[item.label]
parameters[item.variable]["description"] = description
if item.type in (VariableEntityType.TEXT_INPUT, VariableEntityType.PARAGRAPH):
parameters[item.variable]["type"] = "string"
elif item.type == VariableEntityType.SELECT:
parameters[item.variable]["type"] = "string"
parameters[item.variable]["enum"] = item.options
elif item.type == VariableEntityType.NUMBER:
parameters[item.variable]["type"] = "number"
parameters[item.variable]["type"] = "float"
return parameters, required

View File

@ -10,6 +10,7 @@ def init_app(app: DifyApp):
from controllers.console import bp as console_app_bp
from controllers.files import bp as files_bp
from controllers.inner_api import bp as inner_api_bp
from controllers.mcp import bp as mcp_bp
from controllers.service_api import bp as service_api_bp
from controllers.web import bp as web_bp
@ -46,3 +47,4 @@ def init_app(app: DifyApp):
app.register_blueprint(files_bp)
app.register_blueprint(inner_api_bp)
app.register_blueprint(mcp_bp)

View File

@ -10,7 +10,7 @@ from dify_app import DifyApp
from extensions.ext_database import db
from libs.passport import PassportService
from models.account import Account, Tenant, TenantAccountJoin
from models.model import EndUser
from models.model import AppMCPServer, EndUser
from services.account_service import AccountService
login_manager = flask_login.LoginManager()
@ -71,6 +71,21 @@ def load_user_from_request(request_from_flask_login):
if not end_user:
raise NotFound("End user not found.")
return end_user
elif request.blueprint == "mcp":
server_code = request.view_args.get("server_code") if request.view_args else None
if not server_code:
raise Unauthorized("Invalid Authorization token.")
app_mcp_server = db.session.query(AppMCPServer).filter(AppMCPServer.server_code == server_code).first()
if not app_mcp_server:
raise NotFound("App MCP server not found.")
end_user = (
db.session.query(EndUser)
.filter(EndUser.external_user_id == app_mcp_server.id, EndUser.type == "mcp")
.first()
)
if not end_user:
raise NotFound("End user not found.")
return end_user
@user_logged_in.connect