test_mcp_protocol.py 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168
  1. import io
  2. import json
  3. import unittest
  4. from app import GatewayApp
  5. from services.token_store import InMemoryTokenStore
  6. from mcp_protocol import McpProtocolHandler
  7. class DummyApiClient:
  8. def __init__(self):
  9. self.calls = []
  10. def call_tool(self, tool_code, route_path, payload, request_id):
  11. self.calls.append(
  12. {
  13. 'tool_code': tool_code,
  14. 'route_path': route_path,
  15. 'payload': payload,
  16. 'request_id': request_id,
  17. }
  18. )
  19. return {
  20. 'code': 'MCP_0000',
  21. 'msg': 'success',
  22. 'data': {
  23. 'summary': 'matched 1 order',
  24. 'records': [
  25. {
  26. 'order_no': 'SO20260706001',
  27. }
  28. ],
  29. 'tips': ['scoped by employee permissions'],
  30. },
  31. 'meta': {
  32. 'request_id': request_id,
  33. },
  34. }
  35. class McpProtocolTest(unittest.TestCase):
  36. def build_handler(self):
  37. token_store = InMemoryTokenStore(refresh_skew_seconds=60)
  38. token_store.save('MT_demo', '2099-01-01T00:00:00')
  39. app = GatewayApp(
  40. auth_client=None,
  41. api_client=DummyApiClient(),
  42. token_store=token_store,
  43. )
  44. return McpProtocolHandler(app)
  45. def test_initialize_returns_server_capabilities(self):
  46. handler = self.build_handler()
  47. response = handler.handle_request(
  48. {
  49. 'jsonrpc': '2.0',
  50. 'id': 1,
  51. 'method': 'initialize',
  52. 'params': {
  53. 'protocolVersion': '2025-06-18',
  54. 'capabilities': {},
  55. 'clientInfo': {'name': 'workbuddy', 'version': '1.0.0'},
  56. },
  57. }
  58. )
  59. self.assertEqual('2.0', response['jsonrpc'])
  60. self.assertEqual(1, response['id'])
  61. self.assertEqual('2025-06-18', response['result']['protocolVersion'])
  62. self.assertIn('tools', response['result']['capabilities'])
  63. self.assertEqual('fms-mcp-gateway', response['result']['serverInfo']['name'])
  64. def test_initialized_notification_does_not_emit_response(self):
  65. handler = self.build_handler()
  66. response = handler.handle_message(
  67. {
  68. 'jsonrpc': '2.0',
  69. 'method': 'notifications/initialized',
  70. }
  71. )
  72. self.assertIsNone(response)
  73. def test_tools_list_returns_registered_tools(self):
  74. handler = self.build_handler()
  75. response = handler.handle_request(
  76. {
  77. 'jsonrpc': '2.0',
  78. 'id': 2,
  79. 'method': 'tools/list',
  80. 'params': {},
  81. }
  82. )
  83. self.assertEqual('2.0', response['jsonrpc'])
  84. self.assertEqual(2, response['id'])
  85. by_name = {tool['name']: tool for tool in response['result']['tools']}
  86. self.assertIn('query_order', by_name)
  87. self.assertIn('bind_auth_code', by_name)
  88. self.assertIn('inputSchema', by_name['query_order'])
  89. def test_tools_call_wraps_gateway_result_as_structured_content(self):
  90. handler = self.build_handler()
  91. response = handler.handle_request(
  92. {
  93. 'jsonrpc': '2.0',
  94. 'id': 3,
  95. 'method': 'tools/call',
  96. 'params': {
  97. 'name': 'query_order',
  98. 'arguments': {
  99. 'keyword': 'SO20260706001',
  100. 'page': 1,
  101. 'limit': 20,
  102. },
  103. },
  104. }
  105. )
  106. self.assertEqual('2.0', response['jsonrpc'])
  107. self.assertEqual(3, response['id'])
  108. self.assertFalse(response['result']['isError'])
  109. self.assertEqual('matched 1 order', response['result']['structuredContent']['summary'])
  110. self.assertEqual('SO20260706001', response['result']['structuredContent']['records'][0]['order_no'])
  111. self.assertEqual('text', response['result']['content'][0]['type'])
  112. self.assertIn('matched 1 order', response['result']['content'][0]['text'])
  113. def test_run_stdio_writes_only_request_responses(self):
  114. handler = self.build_handler()
  115. stdin = io.StringIO(
  116. json.dumps(
  117. {
  118. 'jsonrpc': '2.0',
  119. 'id': 1,
  120. 'method': 'initialize',
  121. 'params': {
  122. 'protocolVersion': '2025-06-18',
  123. 'capabilities': {},
  124. 'clientInfo': {'name': 'workbuddy', 'version': '1.0.0'},
  125. },
  126. }
  127. )
  128. + '\n'
  129. + json.dumps(
  130. {
  131. 'jsonrpc': '2.0',
  132. 'method': 'notifications/initialized',
  133. }
  134. )
  135. + '\n'
  136. )
  137. stdout = io.StringIO()
  138. handler.run_stdio(stdin=stdin, stdout=stdout)
  139. lines = [line for line in stdout.getvalue().splitlines() if line.strip()]
  140. self.assertEqual(1, len(lines))
  141. response = json.loads(lines[0])
  142. self.assertEqual(1, response['id'])
  143. self.assertEqual('2025-06-18', response['result']['protocolVersion'])
  144. if __name__ == '__main__':
  145. unittest.main()