import json import os import subprocess from pathlib import Path import pytest from app.core.mcp.runtime import identity_from_env from app.core.mcp.servers import CONTEXT_TOOL_NAMES, SCHEDULING_TOOL_NAMES ROOT = Path(__file__).resolve().parents[2] def runtime_env(): return { **os.environ, "PYTHONPATH": str(ROOT), "DATAOPS_MCP_SUBJECT": "agent-context-test", "DATAOPS_MCP_ROLES": "viewer", "DATAOPS_MCP_BUSINESS_DOMAINS": "sales", "DATAOPS_MCP_ENVIRONMENTS": "test", "DATAOPS_MCP_CORRELATION_ID": ("01900000-0000-7000-8000-000000000099"), } def test_identity_from_env_is_explicit_bounded_and_fail_closed(): identity = identity_from_env(runtime_env()) assert identity.subject == "agent-context-test" assert identity.roles == frozenset({"viewer"}) assert identity.business_domains == frozenset({"sales"}) assert identity.environments == frozenset({"test"}) incomplete = runtime_env() incomplete.pop("DATAOPS_MCP_BUSINESS_DOMAINS") with pytest.raises(ValueError, match="BUSINESS_DOMAINS"): identity_from_env(incomplete) unsafe = runtime_env() unsafe["DATAOPS_MCP_ROLES"] = "viewer,root" with pytest.raises(ValueError, match="role"): identity_from_env(unsafe) def test_context_stdio_initializes_and_exposes_only_context_tools(): messages = [ { "jsonrpc": "2.0", "id": 1, "method": "initialize", "params": { "protocolVersion": "2025-06-18", "capabilities": {}, "clientInfo": {"name": "dataops-v53-test", "version": "1.0"}, }, }, { "jsonrpc": "2.0", "method": "notifications/initialized", "params": {}, }, {"jsonrpc": "2.0", "id": 2, "method": "tools/list", "params": {}}, ] result = subprocess.run( [ str(ROOT / ".venv/bin/python"), str(ROOT / "mcp-servers/dataops_mcp_stdio.py"), "--kind", "context", "--factory", "tests.mcp.stdio_fixture:build_context_service", "--allow-test-factory", ], cwd=ROOT, env=runtime_env(), input="\n".join(json.dumps(item) for item in messages) + "\n", capture_output=True, text=True, timeout=30, check=True, ) responses = { item["id"]: item for line in result.stdout.splitlines() if line.strip() for item in [json.loads(line)] if "id" in item } assert responses[1]["result"]["serverInfo"]["name"] == ("DataOps Context MCP") assert { tool["name"] for tool in responses[2]["result"]["tools"] } == CONTEXT_TOOL_NAMES def test_scheduling_stdio_initializes_and_exposes_only_compound_tools(): messages = [ { "jsonrpc": "2.0", "id": 1, "method": "initialize", "params": { "protocolVersion": "2025-06-18", "capabilities": {}, "clientInfo": {"name": "dataops-v53-test", "version": "1.0"}, }, }, { "jsonrpc": "2.0", "method": "notifications/initialized", "params": {}, }, {"jsonrpc": "2.0", "id": 2, "method": "tools/list", "params": {}}, ] result = subprocess.run( [ str(ROOT / ".venv/bin/python"), str(ROOT / "mcp-servers/dataops_mcp_stdio.py"), "--kind", "scheduling", "--factory", "tests.mcp.stdio_fixture:build_scheduling_gateway", "--allow-test-factory", ], cwd=ROOT, env=runtime_env(), input="\n".join(json.dumps(item) for item in messages) + "\n", capture_output=True, text=True, timeout=30, ) assert result.returncode == 0, result.stderr responses = { item["id"]: item for line in result.stdout.splitlines() if line.strip() for item in [json.loads(line)] if "id" in item } assert { tool["name"] for tool in responses[2]["result"]["tools"] } == SCHEDULING_TOOL_NAMES