mcp_protocol.py 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216
  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. structured_content = tool_result.get('data') or {}
  50. return self._success_response(
  51. request_id,
  52. {
  53. 'content': [
  54. {
  55. 'type': 'text',
  56. 'text': self._render_text(structured_content),
  57. }
  58. ],
  59. 'structuredContent': structured_content,
  60. 'isError': False,
  61. },
  62. )
  63. return self._error_response(request_id, -32601, 'Method not found: {0}'.format(method or '<empty>'))
  64. except Exception as exc:
  65. if method == 'tools/call':
  66. return self._success_response(
  67. request_id,
  68. {
  69. 'content': [
  70. {
  71. 'type': 'text',
  72. 'text': str(exc),
  73. }
  74. ],
  75. 'isError': True,
  76. },
  77. )
  78. return self._error_response(request_id, -32000, str(exc))
  79. def run_stdio(self, stdin=None, stdout=None):
  80. stdin = stdin or sys.stdin
  81. stdout = stdout or sys.stdout
  82. for raw_line in stdin:
  83. line = str(raw_line).strip()
  84. if not line:
  85. continue
  86. try:
  87. message = json.loads(line)
  88. except ValueError:
  89. response = self._error_response(None, -32700, 'Parse error')
  90. else:
  91. response = self.handle_message(message)
  92. if response is None:
  93. continue
  94. stdout.write(json.dumps(response, ensure_ascii=True) + '\n')
  95. if hasattr(stdout, 'flush'):
  96. stdout.flush()
  97. return 0
  98. def _normalize_tool(self, tool):
  99. normalized = dict(tool)
  100. if 'input_schema' in normalized:
  101. normalized['inputSchema'] = normalized.pop('input_schema')
  102. return normalized
  103. @staticmethod
  104. def _render_text(structured_content):
  105. if not structured_content:
  106. return 'ok'
  107. columns = structured_content.get('columns') if isinstance(structured_content, dict) else None
  108. records = structured_content.get('records') if isinstance(structured_content, dict) else None
  109. if isinstance(columns, list) and isinstance(records, list):
  110. return McpProtocolHandler._render_table_like_text(structured_content, columns, records)
  111. if isinstance(records, list) and records:
  112. # Check if this is a track-like structure (no columns, but has records)
  113. first_record = records[0] if records else {}
  114. if 'status' in first_record and 'content' in first_record and 'time' in first_record:
  115. return McpProtocolHandler._render_track_like_text(structured_content, records)
  116. return json.dumps(structured_content, ensure_ascii=False)
  117. @staticmethod
  118. def _render_table_like_text(structured_content, columns, records):
  119. lines = []
  120. summary = str(structured_content.get('summary') or '').strip()
  121. if summary:
  122. lines.append(summary)
  123. lines.append('表头共 {0} 列:'.format(len(columns)))
  124. for index, column in enumerate(columns, start=1):
  125. key = str(column.get('key') or '').strip()
  126. name = str(column.get('name') or key).strip()
  127. lines.append('{0}. {1} ({2})'.format(index, name, key))
  128. if not records:
  129. tips = structured_content.get('tips') or []
  130. if tips:
  131. lines.append('提示:{0}'.format(';'.join(str(tip) for tip in tips)))
  132. return '\n'.join(lines)
  133. for record_index, record in enumerate(records, start=1):
  134. lines.append('订单 {0}:'.format(record_index))
  135. for column in columns:
  136. key = str(column.get('key') or '').strip()
  137. name = str(column.get('name') or key).strip()
  138. value = record.get(key, '') if isinstance(record, dict) else ''
  139. if value is None:
  140. value = ''
  141. lines.append('- {0}: {1}'.format(name, value))
  142. tips = structured_content.get('tips') or []
  143. if tips:
  144. lines.append('提示:{0}'.format(';'.join(str(tip) for tip in tips)))
  145. return '\n'.join(lines)
  146. @staticmethod
  147. def _render_track_like_text(structured_content, records):
  148. lines = []
  149. summary = str(structured_content.get('summary') or '').strip()
  150. if summary:
  151. lines.append(summary)
  152. if not records:
  153. tips = structured_content.get('tips') or []
  154. if tips:
  155. lines.append('提示:{0}'.format(';'.join(str(tip) for tip in tips)))
  156. return '\n'.join(lines)
  157. for record_index, record in enumerate(records, start=1):
  158. status = str(record.get('status') or '').strip()
  159. content = str(record.get('content') or '').strip()
  160. location = str(record.get('location') or '').strip()
  161. time = str(record.get('time') or '').strip()
  162. tracking_number = str(record.get('tracking_number') or '').strip()
  163. shipment_id = str(record.get('shipment_id') or '').strip()
  164. sub_track = int(record.get('sub_track') or 0)
  165. lines.append('轨迹 {0}:'.format(record_index))
  166. lines.append('- 状态: {0}'.format(status or '无'))
  167. lines.append('- 内容: {0}'.format(content or '无'))
  168. if location:
  169. lines.append('- 地点: {0}'.format(location))
  170. lines.append('- 时间: {0}'.format(time or '无'))
  171. if tracking_number:
  172. lines.append('- 快递单号: {0}'.format(tracking_number))
  173. if shipment_id:
  174. lines.append('- Shipment ID: {0}'.format(shipment_id))
  175. if sub_track:
  176. lines.append('- 子单轨迹: 是')
  177. tips = structured_content.get('tips') or []
  178. if tips:
  179. lines.append('提示:{0}'.format(';'.join(str(tip) for tip in tips)))
  180. return '\n'.join(lines)
  181. @staticmethod
  182. def _success_response(request_id, result):
  183. return {
  184. 'jsonrpc': '2.0',
  185. 'id': request_id,
  186. 'result': result,
  187. }
  188. @staticmethod
  189. def _error_response(request_id, code, message):
  190. return {
  191. 'jsonrpc': '2.0',
  192. 'id': request_id,
  193. 'error': {
  194. 'code': code,
  195. 'message': message,
  196. },
  197. }