feat(mcp): add integration and discoverability coverage for Jenkins/GlitchTip
This commit is contained in:
@@ -0,0 +1,260 @@
|
||||
"""Tests for MCP discoverability validation (Issue #155)."""
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import tempfile
|
||||
import unittest
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
sys.path.insert(0, str(__import__("pathlib").Path(__file__).resolve().parent.parent))
|
||||
|
||||
from mcp_discoverability import (
|
||||
validate_mcp_client_config,
|
||||
RELOAD_INSTRUCTIONS,
|
||||
EXPECTED_JENKINS_TOOLS,
|
||||
EXPECTED_GLITCHTIP_TOOLS,
|
||||
)
|
||||
|
||||
class TestMcpDiscoverability(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.tmp_dir = tempfile.TemporaryDirectory()
|
||||
self.gitea_config_path = os.path.join(self.tmp_dir.name, "gitea-mcp.json")
|
||||
self.client_config_path = os.path.join(self.tmp_dir.name, "claude_desktop_config.json")
|
||||
|
||||
def tearDown(self):
|
||||
self.tmp_dir.cleanup()
|
||||
|
||||
def _write_json(self, path, data):
|
||||
with open(path, "w", encoding="utf-8") as fh:
|
||||
json.dump(data, fh)
|
||||
|
||||
def _v2_gitea_config(self, jenkins_enabled=True, glitchtip_enabled=False):
|
||||
return {
|
||||
"version": 2,
|
||||
"contexts": {
|
||||
"prod": {
|
||||
"enabled": True,
|
||||
"services": {
|
||||
"jenkins": {
|
||||
"enabled": jenkins_enabled,
|
||||
"kind": "jenkins",
|
||||
"capabilities": ["read"]
|
||||
},
|
||||
"glitchtip": {
|
||||
"enabled": glitchtip_enabled,
|
||||
"kind": "glitchtip",
|
||||
"capabilities": ["read"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def test_static_validation_success(self):
|
||||
g_cfg = self._v2_gitea_config(jenkins_enabled=True, glitchtip_enabled=True)
|
||||
self._write_json(self.gitea_config_path, g_cfg)
|
||||
|
||||
c_cfg = {
|
||||
"mcpServers": {
|
||||
"jenkins-mcp": {
|
||||
"command": "python3",
|
||||
"args": ["-m", "jenkins_mcp"],
|
||||
"env": {
|
||||
"JENKINS_MCP_PROFILE": "jenkins-readonly",
|
||||
"JENKINS_MCP_CONFIG": "/path/to/config.json"
|
||||
}
|
||||
},
|
||||
"glitchtip-mcp": {
|
||||
"command": "python3",
|
||||
"args": ["-m", "glitchtip_mcp"],
|
||||
"env": {
|
||||
"GLITCHTIP_MCP_PROFILE": "glitchtip-readonly",
|
||||
"GLITCHTIP_MCP_CONFIG": "/path/to/config.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
self._write_json(self.client_config_path, c_cfg)
|
||||
|
||||
res = validate_mcp_client_config(
|
||||
client_config_path=self.client_config_path,
|
||||
gitea_config_path=self.gitea_config_path,
|
||||
live=False
|
||||
)
|
||||
self.assertTrue(res)
|
||||
|
||||
def test_stale_server_name_rejected(self):
|
||||
g_cfg = self._v2_gitea_config(jenkins_enabled=True)
|
||||
self._write_json(self.gitea_config_path, g_cfg)
|
||||
|
||||
# Uses stale key in client config
|
||||
c_cfg = {
|
||||
"mcpServers": {
|
||||
"jenkins-readonly": {
|
||||
"command": "python3",
|
||||
"args": ["-m", "jenkins_mcp"],
|
||||
"env": {
|
||||
"JENKINS_MCP_PROFILE": "jenkins-readonly",
|
||||
"JENKINS_MCP_CONFIG": "/path/to/config.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
self._write_json(self.client_config_path, c_cfg)
|
||||
|
||||
res = validate_mcp_client_config(
|
||||
client_config_path=self.client_config_path,
|
||||
gitea_config_path=self.gitea_config_path,
|
||||
live=False
|
||||
)
|
||||
self.assertFalse(res)
|
||||
|
||||
def test_incorrect_arguments_rejected(self):
|
||||
g_cfg = self._v2_gitea_config(jenkins_enabled=True)
|
||||
self._write_json(self.gitea_config_path, g_cfg)
|
||||
|
||||
# Missing -m or wrong module
|
||||
c_cfg = {
|
||||
"mcpServers": {
|
||||
"jenkins-mcp": {
|
||||
"command": "python3",
|
||||
"args": ["wrong_runner.py"],
|
||||
"env": {
|
||||
"JENKINS_MCP_PROFILE": "jenkins-readonly",
|
||||
"JENKINS_MCP_CONFIG": "/path/to/config.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
self._write_json(self.client_config_path, c_cfg)
|
||||
|
||||
res = validate_mcp_client_config(
|
||||
client_config_path=self.client_config_path,
|
||||
gitea_config_path=self.gitea_config_path,
|
||||
live=False
|
||||
)
|
||||
self.assertFalse(res)
|
||||
|
||||
def test_missing_required_env_rejected(self):
|
||||
g_cfg = self._v2_gitea_config(jenkins_enabled=True)
|
||||
self._write_json(self.gitea_config_path, g_cfg)
|
||||
|
||||
# Missing JENKINS_MCP_PROFILE env variable
|
||||
c_cfg = {
|
||||
"mcpServers": {
|
||||
"jenkins-mcp": {
|
||||
"command": "python3",
|
||||
"args": ["-m", "jenkins_mcp"],
|
||||
"env": {
|
||||
"JENKINS_MCP_CONFIG": "/path/to/config.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
self._write_json(self.client_config_path, c_cfg)
|
||||
|
||||
res = validate_mcp_client_config(
|
||||
client_config_path=self.client_config_path,
|
||||
gitea_config_path=self.gitea_config_path,
|
||||
live=False
|
||||
)
|
||||
self.assertFalse(res)
|
||||
|
||||
@patch("subprocess.Popen")
|
||||
def test_live_check_verification_success(self, mock_popen):
|
||||
g_cfg = self._v2_gitea_config(jenkins_enabled=True)
|
||||
self._write_json(self.gitea_config_path, g_cfg)
|
||||
|
||||
c_cfg = {
|
||||
"mcpServers": {
|
||||
"jenkins-mcp": {
|
||||
"command": "python3",
|
||||
"args": ["-m", "jenkins_mcp"],
|
||||
"env": {
|
||||
"JENKINS_MCP_PROFILE": "jenkins-readonly",
|
||||
"JENKINS_MCP_CONFIG": "/path/to/config.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
self._write_json(self.client_config_path, c_cfg)
|
||||
|
||||
# Mock stdout lines for JSON-RPC
|
||||
mock_proc = MagicMock()
|
||||
mock_popen.return_value = mock_proc
|
||||
|
||||
init_resp = json.dumps({"jsonrpc": "2.0", "id": 1, "result": {"protocolVersion": "2024-11-05"}})
|
||||
tools_resp = json.dumps({
|
||||
"jsonrpc": "2.0",
|
||||
"id": 2,
|
||||
"result": {
|
||||
"tools": [{"name": tool} for tool in EXPECTED_JENKINS_TOOLS]
|
||||
}
|
||||
})
|
||||
|
||||
mock_proc.stdout.readline.side_effect = [
|
||||
init_resp + "\n",
|
||||
tools_resp + "\n",
|
||||
""
|
||||
]
|
||||
|
||||
res = validate_mcp_client_config(
|
||||
client_config_path=self.client_config_path,
|
||||
gitea_config_path=self.gitea_config_path,
|
||||
live=True
|
||||
)
|
||||
self.assertTrue(res)
|
||||
|
||||
@patch("subprocess.Popen")
|
||||
def test_live_check_empty_toolset_rejected(self, mock_popen):
|
||||
g_cfg = self._v2_gitea_config(jenkins_enabled=True)
|
||||
self._write_json(self.gitea_config_path, g_cfg)
|
||||
|
||||
c_cfg = {
|
||||
"mcpServers": {
|
||||
"jenkins-mcp": {
|
||||
"command": "python3",
|
||||
"args": ["-m", "jenkins_mcp"],
|
||||
"env": {
|
||||
"JENKINS_MCP_PROFILE": "jenkins-readonly",
|
||||
"JENKINS_MCP_CONFIG": "/path/to/config.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
self._write_json(self.client_config_path, c_cfg)
|
||||
|
||||
mock_proc = MagicMock()
|
||||
mock_popen.return_value = mock_proc
|
||||
|
||||
init_resp = json.dumps({"jsonrpc": "2.0", "id": 1, "result": {}})
|
||||
tools_resp = json.dumps({
|
||||
"jsonrpc": "2.0",
|
||||
"id": 2,
|
||||
"result": {
|
||||
"tools": [] # empty tool list
|
||||
}
|
||||
})
|
||||
|
||||
mock_proc.stdout.readline.side_effect = [
|
||||
init_resp + "\n",
|
||||
tools_resp + "\n",
|
||||
""
|
||||
]
|
||||
|
||||
# Should return False (coverage fails) when no tools are exposed
|
||||
res = validate_mcp_client_config(
|
||||
client_config_path=self.client_config_path,
|
||||
gitea_config_path=self.gitea_config_path,
|
||||
live=True
|
||||
)
|
||||
self.assertFalse(res)
|
||||
|
||||
def test_runbook_instructions_content(self):
|
||||
self.assertIn("MCP CLIENT RELOAD/RECONNECT RUNBOOK", RELOAD_INSTRUCTIONS)
|
||||
self.assertIn("Codex", RELOAD_INSTRUCTIONS)
|
||||
self.assertIn("Gemini", RELOAD_INSTRUCTIONS)
|
||||
self.assertIn("Claude Desktop", RELOAD_INSTRUCTIONS)
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user