mcp_api.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327
  1. # Copyright (c) Alibaba, Inc. and its affiliates.
  2. """
  3. MCP (Model Context Protocol) API interface for ModelScope Hub.
  4. This module provides a simple interface to interact with
  5. ModelScope MCP plaza (https://www.modelscope.cn/mcp).
  6. """
  7. from typing import Any, Dict, Optional
  8. import requests
  9. from modelscope.hub.api import HubApi
  10. from modelscope.hub.errors import raise_for_http_status
  11. from modelscope.utils.logger import get_logger
  12. # Configure logging
  13. logger = get_logger()
  14. # MCP API path
  15. MCP_API_PATH = '/openapi/v1/mcp/servers'
  16. class MCPApiError(Exception):
  17. """Base exception for MCP API errors."""
  18. pass
  19. class MCPApiRequestError(MCPApiError):
  20. """Exception raised when MCP API request fails."""
  21. pass
  22. class MCPApiResponseError(MCPApiError):
  23. """Exception raised when MCP API response is invalid."""
  24. pass
  25. class MCPApi(HubApi):
  26. """
  27. MCP (Model Context Protocol) API interface class.
  28. This class provides interfaces to interact with ModelScope MCP servers,
  29. such as to list, deploy and manage MCP servers.
  30. Note: MCPApi inherits login() from HubApi for authentication.
  31. Different methods have different token requirements - see individual method docs.
  32. """
  33. def __init__(self, endpoint: Optional[str] = None) -> None:
  34. """
  35. Initialize MCP API.
  36. Args:
  37. endpoint: The modelscope server address. Defaults to None (uses default endpoint).
  38. """
  39. super().__init__(endpoint=endpoint)
  40. self.mcp_base_url = self.endpoint + MCP_API_PATH
  41. @staticmethod
  42. def _handle_response(r: requests.Response) -> Dict[str, Any]:
  43. """
  44. Handle HTTP response with unified error handling and JSON parsing.
  45. Args:
  46. r: requests Response object
  47. Returns:
  48. Parsed response data dict
  49. Raises:
  50. MCPApiResponseError: If JSON parsing fails
  51. """
  52. try:
  53. resp = r.json()
  54. except requests.exceptions.JSONDecodeError as e:
  55. logger.error(f'JSON parsing failed: {e}')
  56. logger.error(f'Response content: {r.text}')
  57. raise MCPApiResponseError(f'Invalid JSON response: {e}') from e
  58. return resp.get('data', {})
  59. @staticmethod
  60. def _get_server_name_from_id(server_id: str) -> str:
  61. """Extract server name from server ID."""
  62. if '/' in server_id:
  63. return server_id.split('/', 1)[1]
  64. return server_id
  65. def list_mcp_servers(self,
  66. token: Optional[str] = None,
  67. filter: Optional[Dict[str, Any]] = None,
  68. total_count: Optional[int] = 20,
  69. search: Optional[str] = '') -> Dict[str, Any]:
  70. """
  71. List available MCP servers, if (optional) token is presented, this would return private MCP servers as well.
  72. Args:
  73. token: Optional access token for authentication
  74. filter: Optional filters to apply to the search
  75. - 'category': str, server category, e.g. 'communication'
  76. - 'tag': str, server tag, e.g. 'social-media'
  77. - 'is_hosted': bool, server is hosted
  78. When all three are passed in, the intersection is taken.
  79. total_count: Number of servers to return, max 100, default 20
  80. search: Optional search query string,e.g. Chinese service name, English service name, author/owner username
  81. You can combine `filter` and `search` to retrieve desired MCP servers.
  82. Returns:
  83. Dict containing:
  84. - total_count: Total number of servers
  85. - servers: List of server dictionaries with name, id, description
  86. Raises:
  87. MCPApiRequestError: If API request fails (network, server errors)
  88. MCPApiResponseError: If response format is invalid or JSON parsing fails
  89. Authentication:
  90. Optional, only required if you wish to retrieve private MCP servers.
  91. You may leverage the token parameter for one-time authentication, or use api.login()
  92. Returns:
  93. {
  94. 'total_count': 20,
  95. 'servers': [
  96. {'name': 'ServerA', 'id': '@demo/ServerA', 'description': 'This is a demo server for xxx.'},
  97. {'name': 'ServerB', 'id': '@demo/ServerB', 'description': 'This is another demo server.'},
  98. ...
  99. ]
  100. }
  101. """
  102. if total_count is None or total_count < 1 or total_count > 100:
  103. raise ValueError('total_count must be between 1 and 100')
  104. body = {
  105. 'filter': filter or {},
  106. 'page_number': 1,
  107. 'page_size': total_count,
  108. 'search': search
  109. }
  110. try:
  111. cookies = self.get_cookies(token)
  112. r = self.session.put(
  113. url=self.mcp_base_url,
  114. headers=self.builder_headers(self.headers),
  115. json=body,
  116. cookies=cookies)
  117. raise_for_http_status(r)
  118. except requests.exceptions.RequestException as e:
  119. logger.error('Failed to get MCP servers: %s', e)
  120. raise MCPApiRequestError(f'Failed to get MCP servers: {e}') from e
  121. data = self._handle_response(r)
  122. mcp_server_list = data.get('mcp_server_list', [])
  123. mcp_config_list = [{
  124. 'name': item.get('name', ''),
  125. 'id': item.get('id', ''),
  126. 'description': item.get('description', '')
  127. } for item in mcp_server_list]
  128. return {
  129. 'total_count': data.get('total_count', 0),
  130. 'servers': mcp_config_list
  131. }
  132. def list_operational_mcp_servers(self,
  133. token: str = None) -> Dict[str, Any]:
  134. """
  135. Get list of operational MCP servers that have been triggered hosting service by the user.
  136. Returns:
  137. Dict containing:
  138. - total_counts: Total number of operational servers
  139. - servers: List of server info with name, id, description
  140. Raises:
  141. MCPApiRequestError: If authentication fails or API request fails
  142. MCPApiResponseError: If response format is invalid or JSON parsing fails
  143. Returns:
  144. {
  145. 'total_count': 10,
  146. 'servers': [
  147. {
  148. 'name': 'ServerA',
  149. "id": "@Group1/ServerA",
  150. 'description': 'This is a demo server for xxx.'
  151. 'mcp_servers': [
  152. {
  153. 'type': 'sse',
  154. 'url': 'https://mcp.api-inference.modelscope.net/{uuid}/sse'
  155. },
  156. {
  157. 'type': 'streamable_http',
  158. 'url': 'https://mcp.api-inference.modelscope.net/{uuid}/streamable_http'
  159. },
  160. ...
  161. ]
  162. },
  163. ...
  164. ]
  165. }
  166. """
  167. url = f'{self.mcp_base_url}/operational'
  168. headers = self.builder_headers(self.headers)
  169. try:
  170. cookies = self.get_cookies(
  171. access_token=token, cookies_required=True)
  172. r = self.session.get(url, headers=headers, cookies=cookies)
  173. raise_for_http_status(r)
  174. except requests.exceptions.RequestException as e:
  175. logger.error(f'Failed to get operational MCP servers: {e}')
  176. raise MCPApiRequestError(
  177. f'Failed to get operational MCP servers: {e}') from e
  178. logger.debug(f'Response status code: {r.status_code}')
  179. data = self._handle_response(r)
  180. mcp_server_list = data.get('mcp_server_list', [])
  181. mcp_config_list = []
  182. for item in mcp_server_list:
  183. mcp_config = {}
  184. mcp_config['name'] = item.get('name', '')
  185. mcp_config['id'] = item.get('id', '')
  186. mcp_config['description'] = item.get('description', '')
  187. mcp_config['mcp_servers'] = []
  188. for operational_url in item.get('operational_urls', []):
  189. mcp_config['mcp_servers'].append({
  190. 'type':
  191. operational_url.get('url').split('/')[-1],
  192. 'url':
  193. operational_url.get('url', '')
  194. })
  195. mcp_config_list.append(mcp_config)
  196. return {
  197. 'total_count': data.get('total_count', 0),
  198. 'servers': mcp_config_list
  199. }
  200. def get_mcp_server(self,
  201. server_id: str,
  202. token: Optional[str] = None) -> Dict[str, Any]:
  203. """
  204. Get detailed information for a specific MCP Server,
  205. a valid token shall be provided if the MCP server is private.
  206. Args:
  207. server_id: MCP server ID (e.g., "@amap/amap-maps")
  208. token: Optional access token for authentication
  209. Returns:
  210. Dict containing:
  211. - name: Server name
  212. - description: Server description
  213. - id: Server ID
  214. - service_config: Connection configuration with type and url
  215. Raises:
  216. ValueError: If server_id is empty or None
  217. MCPApiRequestError: If API request fails or server not found
  218. MCPApiResponseError: If response format is invalid or JSON parsing fails
  219. Returns:
  220. {
  221. 'name': 'ServerA',
  222. 'description': 'This is a demo server for xxx.',
  223. 'id': '@demo/serverA',
  224. 'servers': [
  225. {
  226. 'type': 'sse',
  227. 'url': 'https://mcp.api-inference.modelscope.net/{uuid}/sse'
  228. },
  229. {
  230. 'type': 'streamable_http',
  231. 'url': 'https://mcp.api-inference.modelscope.net/{uuid}/streamable_http'
  232. }
  233. ...
  234. ]
  235. }
  236. """
  237. if not server_id:
  238. raise ValueError('server_id cannot be empty')
  239. url = f'{self.mcp_base_url}/{server_id}'
  240. headers = self.builder_headers(self.headers)
  241. try:
  242. cookies = self.get_cookies(token)
  243. r = self.session.get(
  244. url,
  245. headers=headers,
  246. params={'get_operational_url':
  247. True}, # Always get operational URLs
  248. cookies=cookies)
  249. raise_for_http_status(r)
  250. except requests.exceptions.RequestException as e:
  251. logger.error(f'Failed to get MCP server {server_id}: {e}')
  252. raise MCPApiRequestError(
  253. f'Failed to get MCP server {server_id}: {e}') from e
  254. data = self._handle_response(r)
  255. result = {
  256. 'name': data.get('name', ''),
  257. 'description': data.get('description', ''),
  258. 'id': data.get('id', '')
  259. }
  260. server_id = data.get('id', '')
  261. server_name = MCPApi._get_server_name_from_id(server_id)
  262. operational_urls = data.get('operational_urls', [])
  263. mcp_config_list = []
  264. if server_name and operational_urls:
  265. for operational_url in operational_urls:
  266. mcp_config = {
  267. 'type': operational_url.get('url').split('/')[-1],
  268. 'url': operational_url.get('url', '')
  269. }
  270. mcp_config_list.append(mcp_config)
  271. result['servers'] = mcp_config_list
  272. return result