mcp_protocol.py 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255
  1. import json
  2. import sys
  3. class McpProtocolHandler:
  4. protocol_version = '2025-06-18'
  5. server_name = 'fms-mcp-gateway'
  6. server_version = '0.1.0'
  7. def __init__(self, gateway_app):
  8. self.gateway_app = gateway_app
  9. self.initialized = False
  10. def handle_message(self, message):
  11. if not isinstance(message, dict):
  12. return self._error_response(None, -32600, 'Invalid Request')
  13. if 'id' in message:
  14. return self.handle_request(message)
  15. method = str(message.get('method') or '').strip()
  16. if method == 'notifications/initialized':
  17. self.initialized = True
  18. return None
  19. return None
  20. def handle_request(self, request):
  21. request_id = request.get('id')
  22. method = str(request.get('method') or '').strip()
  23. try:
  24. if method == 'initialize':
  25. self.initialized = True
  26. return self._success_response(
  27. request_id,
  28. {
  29. 'protocolVersion': self.protocol_version,
  30. 'capabilities': {
  31. 'tools': {
  32. 'listChanged': False,
  33. }
  34. },
  35. 'serverInfo': {
  36. 'name': self.server_name,
  37. 'version': self.server_version,
  38. },
  39. },
  40. )
  41. if method == 'tools/list':
  42. tools = [self._normalize_tool(tool) for tool in self.gateway_app.list_tools()]
  43. return self._success_response(request_id, {'tools': tools})
  44. if method == 'tools/call':
  45. params = request.get('params') or {}
  46. tool_name = params.get('name')
  47. arguments = params.get('arguments') or {}
  48. tool_result = self.gateway_app.call_tool(tool_name, arguments)
  49. return self._tool_call_response(request_id, tool_result)
  50. return self._error_response(request_id, -32601, 'Method not found: {0}'.format(method or '<empty>'))
  51. except Exception as exc:
  52. if method == 'tools/call':
  53. return self._success_response(
  54. request_id,
  55. {
  56. 'content': [
  57. {
  58. 'type': 'text',
  59. 'text': str(exc),
  60. }
  61. ],
  62. 'isError': True,
  63. },
  64. )
  65. return self._error_response(request_id, -32000, str(exc))
  66. def run_stdio(self, stdin=None, stdout=None):
  67. stdin = stdin or sys.stdin
  68. stdout = stdout or sys.stdout
  69. for raw_line in stdin:
  70. line = str(raw_line).strip()
  71. if not line:
  72. continue
  73. try:
  74. message = json.loads(line)
  75. except ValueError:
  76. response = self._error_response(None, -32700, 'Parse error')
  77. else:
  78. response = self.handle_message(message)
  79. if response is None:
  80. continue
  81. stdout.write(json.dumps(response, ensure_ascii=True) + '\n')
  82. if hasattr(stdout, 'flush'):
  83. stdout.flush()
  84. return 0
  85. def _normalize_tool(self, tool):
  86. normalized = dict(tool)
  87. if 'input_schema' in normalized:
  88. normalized['inputSchema'] = normalized.pop('input_schema')
  89. return normalized
  90. @classmethod
  91. def _tool_call_response(cls, request_id, tool_result):
  92. if not isinstance(tool_result, dict):
  93. raise RuntimeError('invalid tool response')
  94. raw_code = tool_result.get('code')
  95. code = str(raw_code).strip() if raw_code is not None else ''
  96. message = str(tool_result.get('msg') or '').strip()
  97. data = tool_result.get('data')
  98. meta = tool_result.get('meta')
  99. if code in ('MCP_0000', '0'):
  100. if isinstance(data, dict):
  101. structured_content = dict(data)
  102. elif data:
  103. structured_content = {'data': data}
  104. else:
  105. structured_content = {}
  106. if isinstance(meta, dict) and meta:
  107. structured_content['meta'] = dict(meta)
  108. return cls._success_response(request_id, {
  109. 'content': [{
  110. 'type': 'text',
  111. 'text': cls._render_text(structured_content),
  112. }],
  113. 'structuredContent': structured_content,
  114. 'isError': False,
  115. })
  116. error_content = {
  117. 'code': code or 'MCP_9001',
  118. 'msg': message or 'tool call failed',
  119. }
  120. if data not in (None, [], {}):
  121. error_content['data'] = data
  122. if isinstance(meta, dict) and meta:
  123. error_content['meta'] = dict(meta)
  124. return cls._success_response(request_id, {
  125. 'content': [{
  126. 'type': 'text',
  127. 'text': '{0}: {1}'.format(
  128. error_content['code'],
  129. error_content['msg'],
  130. ),
  131. }],
  132. 'structuredContent': error_content,
  133. 'isError': True,
  134. })
  135. @staticmethod
  136. def _render_text(structured_content):
  137. if not structured_content:
  138. return 'ok'
  139. columns = structured_content.get('columns') if isinstance(structured_content, dict) else None
  140. records = structured_content.get('records') if isinstance(structured_content, dict) else None
  141. if isinstance(columns, list) and isinstance(records, list):
  142. return McpProtocolHandler._render_table_like_text(structured_content, columns, records)
  143. if isinstance(records, list) and records:
  144. # Check if this is a track-like structure (no columns, but has records)
  145. first_record = records[0] if records else {}
  146. if 'status' in first_record and 'content' in first_record and 'time' in first_record:
  147. return McpProtocolHandler._render_track_like_text(structured_content, records)
  148. return json.dumps(structured_content, ensure_ascii=False)
  149. @staticmethod
  150. def _render_table_like_text(structured_content, columns, records):
  151. lines = []
  152. summary = str(structured_content.get('summary') or '').strip()
  153. if summary:
  154. lines.append(summary)
  155. lines.append('表头共 {0} 列:'.format(len(columns)))
  156. for index, column in enumerate(columns, start=1):
  157. key = str(column.get('key') or '').strip()
  158. name = str(column.get('name') or key).strip()
  159. lines.append('{0}. {1} ({2})'.format(index, name, key))
  160. if not records:
  161. tips = structured_content.get('tips') or []
  162. if tips:
  163. lines.append('提示:{0}'.format(';'.join(str(tip) for tip in tips)))
  164. return '\n'.join(lines)
  165. for record_index, record in enumerate(records, start=1):
  166. lines.append('订单 {0}:'.format(record_index))
  167. for column in columns:
  168. key = str(column.get('key') or '').strip()
  169. name = str(column.get('name') or key).strip()
  170. value = record.get(key, '') if isinstance(record, dict) else ''
  171. if value is None:
  172. value = ''
  173. lines.append('- {0}: {1}'.format(name, value))
  174. tips = structured_content.get('tips') or []
  175. if tips:
  176. lines.append('提示:{0}'.format(';'.join(str(tip) for tip in tips)))
  177. return '\n'.join(lines)
  178. @staticmethod
  179. def _render_track_like_text(structured_content, records):
  180. lines = []
  181. summary = str(structured_content.get('summary') or '').strip()
  182. if summary:
  183. lines.append(summary)
  184. if not records:
  185. tips = structured_content.get('tips') or []
  186. if tips:
  187. lines.append('提示:{0}'.format(';'.join(str(tip) for tip in tips)))
  188. return '\n'.join(lines)
  189. for record_index, record in enumerate(records, start=1):
  190. status = str(record.get('status') or '').strip()
  191. content = str(record.get('content') or '').strip()
  192. location = str(record.get('location') or '').strip()
  193. time = str(record.get('time') or '').strip()
  194. tracking_number = str(record.get('tracking_number') or '').strip()
  195. shipment_id = str(record.get('shipment_id') or '').strip()
  196. sub_track = int(record.get('sub_track') or 0)
  197. lines.append('轨迹 {0}:'.format(record_index))
  198. lines.append('- 状态: {0}'.format(status or '无'))
  199. lines.append('- 内容: {0}'.format(content or '无'))
  200. if location:
  201. lines.append('- 地点: {0}'.format(location))
  202. lines.append('- 时间: {0}'.format(time or '无'))
  203. if tracking_number:
  204. lines.append('- 快递单号: {0}'.format(tracking_number))
  205. if shipment_id:
  206. lines.append('- Shipment ID: {0}'.format(shipment_id))
  207. if sub_track:
  208. lines.append('- 子单轨迹: 是')
  209. tips = structured_content.get('tips') or []
  210. if tips:
  211. lines.append('提示:{0}'.format(';'.join(str(tip) for tip in tips)))
  212. return '\n'.join(lines)
  213. @staticmethod
  214. def _success_response(request_id, result):
  215. return {
  216. 'jsonrpc': '2.0',
  217. 'id': request_id,
  218. 'result': result,
  219. }
  220. @staticmethod
  221. def _error_response(request_id, code, message):
  222. return {
  223. 'jsonrpc': '2.0',
  224. 'id': request_id,
  225. 'error': {
  226. 'code': code,
  227. 'message': message,
  228. },
  229. }