Nelze vybrat více než 25 témat Téma musí začínat písmenem nebo číslem, může obsahovat pomlčky („-“) a může být dlouhé až 35 znaků.

mcp.py 10KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244
  1. from typing import Optional, Union
  2. from flask import Response
  3. from flask_restx import Resource, reqparse
  4. from pydantic import ValidationError
  5. from sqlalchemy.orm import Session
  6. from controllers.console.app.mcp_server import AppMCPServerStatus
  7. from controllers.mcp import mcp_ns
  8. from core.app.app_config.entities import VariableEntity
  9. from core.mcp import types as mcp_types
  10. from core.mcp.server.streamable_http import handle_mcp_request
  11. from extensions.ext_database import db
  12. from libs import helper
  13. from models.model import App, AppMCPServer, AppMode, EndUser
  14. class MCPRequestError(Exception):
  15. """Custom exception for MCP request processing errors"""
  16. def __init__(self, error_code: int, message: str):
  17. self.error_code = error_code
  18. self.message = message
  19. super().__init__(message)
  20. def int_or_str(value):
  21. """Validate that a value is either an integer or string."""
  22. if isinstance(value, (int, str)):
  23. return value
  24. else:
  25. return None
  26. # Define parser for both documentation and validation
  27. mcp_request_parser = reqparse.RequestParser()
  28. mcp_request_parser.add_argument(
  29. "jsonrpc", type=str, required=True, location="json", help="JSON-RPC version (should be '2.0')"
  30. )
  31. mcp_request_parser.add_argument("method", type=str, required=True, location="json", help="The method to invoke")
  32. mcp_request_parser.add_argument("params", type=dict, required=False, location="json", help="Parameters for the method")
  33. mcp_request_parser.add_argument(
  34. "id", type=int_or_str, required=False, location="json", help="Request ID for tracking responses"
  35. )
  36. @mcp_ns.route("/server/<string:server_code>/mcp")
  37. class MCPAppApi(Resource):
  38. @mcp_ns.expect(mcp_request_parser)
  39. @mcp_ns.doc("handle_mcp_request")
  40. @mcp_ns.doc(description="Handle Model Context Protocol (MCP) requests for a specific server")
  41. @mcp_ns.doc(params={"server_code": "Unique identifier for the MCP server"})
  42. @mcp_ns.doc(
  43. responses={
  44. 200: "MCP response successfully processed",
  45. 400: "Invalid MCP request or parameters",
  46. 404: "Server or app not found",
  47. }
  48. )
  49. def post(self, server_code: str):
  50. """Handle MCP requests for a specific server.
  51. Processes JSON-RPC formatted requests according to the Model Context Protocol specification.
  52. Validates the server status and associated app before processing the request.
  53. Args:
  54. server_code: Unique identifier for the MCP server
  55. Returns:
  56. dict: JSON-RPC response from the MCP handler
  57. Raises:
  58. ValidationError: Invalid request format or parameters
  59. """
  60. args = mcp_request_parser.parse_args()
  61. request_id: Optional[Union[int, str]] = args.get("id")
  62. mcp_request = self._parse_mcp_request(args)
  63. with Session(db.engine, expire_on_commit=False) as session:
  64. # Get MCP server and app
  65. mcp_server, app = self._get_mcp_server_and_app(server_code, session)
  66. self._validate_server_status(mcp_server)
  67. # Get user input form
  68. user_input_form = self._get_user_input_form(app)
  69. # Handle notification vs request differently
  70. return self._process_mcp_message(mcp_request, request_id, app, mcp_server, user_input_form, session)
  71. def _get_mcp_server_and_app(self, server_code: str, session: Session) -> tuple[AppMCPServer, App]:
  72. """Get and validate MCP server and app in one query session"""
  73. mcp_server = session.query(AppMCPServer).where(AppMCPServer.server_code == server_code).first()
  74. if not mcp_server:
  75. raise MCPRequestError(mcp_types.INVALID_REQUEST, "Server Not Found")
  76. app = session.query(App).where(App.id == mcp_server.app_id).first()
  77. if not app:
  78. raise MCPRequestError(mcp_types.INVALID_REQUEST, "App Not Found")
  79. return mcp_server, app
  80. def _validate_server_status(self, mcp_server: AppMCPServer):
  81. """Validate MCP server status"""
  82. if mcp_server.status != AppMCPServerStatus.ACTIVE:
  83. raise MCPRequestError(mcp_types.INVALID_REQUEST, "Server is not active")
  84. def _process_mcp_message(
  85. self,
  86. mcp_request: mcp_types.ClientRequest | mcp_types.ClientNotification,
  87. request_id: Optional[Union[int, str]],
  88. app: App,
  89. mcp_server: AppMCPServer,
  90. user_input_form: list[VariableEntity],
  91. session: Session,
  92. ) -> Response:
  93. """Process MCP message (notification or request)"""
  94. if isinstance(mcp_request, mcp_types.ClientNotification):
  95. return self._handle_notification(mcp_request)
  96. else:
  97. return self._handle_request(mcp_request, request_id, app, mcp_server, user_input_form, session)
  98. def _handle_notification(self, mcp_request: mcp_types.ClientNotification) -> Response:
  99. """Handle MCP notification"""
  100. # For notifications, only support init notification
  101. if mcp_request.root.method != "notifications/initialized":
  102. raise MCPRequestError(mcp_types.INVALID_REQUEST, "Invalid notification method")
  103. # Return HTTP 202 Accepted for notifications (no response body)
  104. return Response("", status=202, content_type="application/json")
  105. def _handle_request(
  106. self,
  107. mcp_request: mcp_types.ClientRequest,
  108. request_id: Optional[Union[int, str]],
  109. app: App,
  110. mcp_server: AppMCPServer,
  111. user_input_form: list[VariableEntity],
  112. session: Session,
  113. ) -> Response:
  114. """Handle MCP request"""
  115. if request_id is None:
  116. raise MCPRequestError(mcp_types.INVALID_REQUEST, "Request ID is required")
  117. result = self._handle_mcp_request(app, mcp_server, mcp_request, user_input_form, session, request_id)
  118. if result is None:
  119. # This shouldn't happen for requests, but handle gracefully
  120. raise MCPRequestError(mcp_types.INTERNAL_ERROR, "No response generated for request")
  121. return helper.compact_generate_response(result.model_dump(by_alias=True, mode="json", exclude_none=True))
  122. def _get_user_input_form(self, app: App) -> list[VariableEntity]:
  123. """Get and convert user input form"""
  124. # Get raw user input form based on app mode
  125. if app.mode in {AppMode.ADVANCED_CHAT, AppMode.WORKFLOW}:
  126. if not app.workflow:
  127. raise MCPRequestError(mcp_types.INVALID_REQUEST, "App is unavailable")
  128. raw_user_input_form = app.workflow.user_input_form(to_old_structure=True)
  129. else:
  130. if not app.app_model_config:
  131. raise MCPRequestError(mcp_types.INVALID_REQUEST, "App is unavailable")
  132. features_dict = app.app_model_config.to_dict()
  133. raw_user_input_form = features_dict.get("user_input_form", [])
  134. # Convert to VariableEntity objects
  135. try:
  136. return self._convert_user_input_form(raw_user_input_form)
  137. except ValidationError as e:
  138. raise MCPRequestError(mcp_types.INVALID_PARAMS, f"Invalid user_input_form: {str(e)}")
  139. def _convert_user_input_form(self, raw_form: list[dict]) -> list[VariableEntity]:
  140. """Convert raw user input form to VariableEntity objects"""
  141. return [self._create_variable_entity(item) for item in raw_form]
  142. def _create_variable_entity(self, item: dict) -> VariableEntity:
  143. """Create a single VariableEntity from raw form item"""
  144. variable_type = item.get("type", "") or list(item.keys())[0]
  145. variable = item[variable_type]
  146. return VariableEntity(
  147. type=variable_type,
  148. variable=variable.get("variable"),
  149. description=variable.get("description") or "",
  150. label=variable.get("label"),
  151. required=variable.get("required", False),
  152. max_length=variable.get("max_length"),
  153. options=variable.get("options") or [],
  154. )
  155. def _parse_mcp_request(self, args: dict) -> mcp_types.ClientRequest | mcp_types.ClientNotification:
  156. """Parse and validate MCP request"""
  157. try:
  158. return mcp_types.ClientRequest.model_validate(args)
  159. except ValidationError:
  160. try:
  161. return mcp_types.ClientNotification.model_validate(args)
  162. except ValidationError as e:
  163. raise MCPRequestError(mcp_types.INVALID_PARAMS, f"Invalid MCP request: {str(e)}")
  164. def _retrieve_end_user(self, tenant_id: str, mcp_server_id: str, session: Session) -> EndUser | None:
  165. """Get end user from existing session - optimized query"""
  166. return (
  167. session.query(EndUser)
  168. .where(EndUser.tenant_id == tenant_id)
  169. .where(EndUser.session_id == mcp_server_id)
  170. .where(EndUser.type == "mcp")
  171. .first()
  172. )
  173. def _create_end_user(
  174. self, client_name: str, tenant_id: str, app_id: str, mcp_server_id: str, session: Session
  175. ) -> EndUser:
  176. """Create end user in existing session"""
  177. end_user = EndUser(
  178. tenant_id=tenant_id,
  179. app_id=app_id,
  180. type="mcp",
  181. name=client_name,
  182. session_id=mcp_server_id,
  183. )
  184. session.add(end_user)
  185. session.flush() # Use flush instead of commit to keep transaction open
  186. session.refresh(end_user)
  187. return end_user
  188. def _handle_mcp_request(
  189. self,
  190. app: App,
  191. mcp_server: AppMCPServer,
  192. mcp_request: mcp_types.ClientRequest,
  193. user_input_form: list[VariableEntity],
  194. session: Session,
  195. request_id: Union[int, str],
  196. ) -> mcp_types.JSONRPCResponse | mcp_types.JSONRPCError | None:
  197. """Handle MCP request and return response"""
  198. end_user = self._retrieve_end_user(mcp_server.tenant_id, mcp_server.id, session)
  199. if not end_user and isinstance(mcp_request.root, mcp_types.InitializeRequest):
  200. client_info = mcp_request.root.params.clientInfo
  201. client_name = f"{client_info.name}@{client_info.version}"
  202. # Commit the session before creating end user to avoid transaction conflicts
  203. session.commit()
  204. with Session(db.engine, expire_on_commit=False) as create_session, create_session.begin():
  205. end_user = self._create_end_user(client_name, app.tenant_id, app.id, mcp_server.id, create_session)
  206. return handle_mcp_request(app, mcp_request, user_input_form, mcp_server, end_user, request_id)