test_stdio_runtime.py 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143
  1. import json
  2. import os
  3. import subprocess
  4. from pathlib import Path
  5. import pytest
  6. from app.core.mcp.runtime import identity_from_env
  7. from app.core.mcp.servers import CONTEXT_TOOL_NAMES, SCHEDULING_TOOL_NAMES
  8. ROOT = Path(__file__).resolve().parents[2]
  9. def runtime_env():
  10. return {
  11. **os.environ,
  12. "PYTHONPATH": str(ROOT),
  13. "DATAOPS_MCP_SUBJECT": "agent-context-test",
  14. "DATAOPS_MCP_ROLES": "viewer",
  15. "DATAOPS_MCP_BUSINESS_DOMAINS": "sales",
  16. "DATAOPS_MCP_ENVIRONMENTS": "test",
  17. "DATAOPS_MCP_CORRELATION_ID": ("01900000-0000-7000-8000-000000000099"),
  18. }
  19. def test_identity_from_env_is_explicit_bounded_and_fail_closed():
  20. identity = identity_from_env(runtime_env())
  21. assert identity.subject == "agent-context-test"
  22. assert identity.roles == frozenset({"viewer"})
  23. assert identity.business_domains == frozenset({"sales"})
  24. assert identity.environments == frozenset({"test"})
  25. incomplete = runtime_env()
  26. incomplete.pop("DATAOPS_MCP_BUSINESS_DOMAINS")
  27. with pytest.raises(ValueError, match="BUSINESS_DOMAINS"):
  28. identity_from_env(incomplete)
  29. unsafe = runtime_env()
  30. unsafe["DATAOPS_MCP_ROLES"] = "viewer,root"
  31. with pytest.raises(ValueError, match="role"):
  32. identity_from_env(unsafe)
  33. def test_context_stdio_initializes_and_exposes_only_context_tools():
  34. messages = [
  35. {
  36. "jsonrpc": "2.0",
  37. "id": 1,
  38. "method": "initialize",
  39. "params": {
  40. "protocolVersion": "2025-06-18",
  41. "capabilities": {},
  42. "clientInfo": {"name": "dataops-v53-test", "version": "1.0"},
  43. },
  44. },
  45. {
  46. "jsonrpc": "2.0",
  47. "method": "notifications/initialized",
  48. "params": {},
  49. },
  50. {"jsonrpc": "2.0", "id": 2, "method": "tools/list", "params": {}},
  51. ]
  52. result = subprocess.run(
  53. [
  54. str(ROOT / ".venv/bin/python"),
  55. str(ROOT / "mcp-servers/dataops_mcp_stdio.py"),
  56. "--kind",
  57. "context",
  58. "--factory",
  59. "tests.mcp.stdio_fixture:build_context_service",
  60. "--allow-test-factory",
  61. ],
  62. cwd=ROOT,
  63. env=runtime_env(),
  64. input="\n".join(json.dumps(item) for item in messages) + "\n",
  65. capture_output=True,
  66. text=True,
  67. timeout=30,
  68. check=True,
  69. )
  70. responses = {
  71. item["id"]: item
  72. for line in result.stdout.splitlines()
  73. if line.strip()
  74. for item in [json.loads(line)]
  75. if "id" in item
  76. }
  77. assert responses[1]["result"]["serverInfo"]["name"] == ("DataOps Context MCP")
  78. assert {
  79. tool["name"] for tool in responses[2]["result"]["tools"]
  80. } == CONTEXT_TOOL_NAMES
  81. def test_scheduling_stdio_initializes_and_exposes_only_compound_tools():
  82. messages = [
  83. {
  84. "jsonrpc": "2.0",
  85. "id": 1,
  86. "method": "initialize",
  87. "params": {
  88. "protocolVersion": "2025-06-18",
  89. "capabilities": {},
  90. "clientInfo": {"name": "dataops-v53-test", "version": "1.0"},
  91. },
  92. },
  93. {
  94. "jsonrpc": "2.0",
  95. "method": "notifications/initialized",
  96. "params": {},
  97. },
  98. {"jsonrpc": "2.0", "id": 2, "method": "tools/list", "params": {}},
  99. ]
  100. result = subprocess.run(
  101. [
  102. str(ROOT / ".venv/bin/python"),
  103. str(ROOT / "mcp-servers/dataops_mcp_stdio.py"),
  104. "--kind",
  105. "scheduling",
  106. "--factory",
  107. "tests.mcp.stdio_fixture:build_scheduling_gateway",
  108. "--allow-test-factory",
  109. ],
  110. cwd=ROOT,
  111. env=runtime_env(),
  112. input="\n".join(json.dumps(item) for item in messages) + "\n",
  113. capture_output=True,
  114. text=True,
  115. timeout=30,
  116. )
  117. assert result.returncode == 0, result.stderr
  118. responses = {
  119. item["id"]: item
  120. for line in result.stdout.splitlines()
  121. if line.strip()
  122. for item in [json.loads(line)]
  123. if "id" in item
  124. }
  125. assert {
  126. tool["name"] for tool in responses[2]["result"]["tools"]
  127. } == SCHEDULING_TOOL_NAMES