Skip to content

Commit a8c6422

Browse files
committed
Merge branch 'develop'
2 parents 5894769 + f05b6a0 commit a8c6422

File tree

3 files changed

+154
-1
lines changed

3 files changed

+154
-1
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ The client included in the project is only for testing how the code works; we re
1717
## Supported MCP Tools
1818

1919
* `get_status`: Performs a health check on the configured SonarQube instance.
20+
* `create_sonarqube_project`: Creates a new SonarQube project. Requires administrator privileges.
21+
* `delete_sonarqube_project`: Deletes a SonarQube project. Requires administrator privileges. **USE WITH CAUTION!**
2022
* `list_projects`: Lists all accessible SonarQube projects, optionally filtered by name or key.
2123
* `get_sonarqube_metrics`: Retrieves specified metrics (bugs, vulnerabilities, code smells, coverage, duplication density) for a given SonarQube project key.
2224
* `get_sonarqube_metrics_history`: Retrieves historical metrics (bugs, vulnerabilities, code smells, coverage, duplication density) for a given SonarQube project using /api/measures/search_history. Optional date filters can be applied.

client_test.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,45 @@ async def run_test(project_key: str):
243243
print(
244244
"Check if the server script can run independently and if dependencies are met."
245245
)
246+
247+
# DANGER ZONE! UNCOMMENT TO TRY IT!
248+
249+
# try:
250+
# async with client:
251+
# print("\n--- Testing create_sonarqube_project ---")
252+
# new_project_key = "test-project-mcp-delete-me" # Unique key
253+
# new_project_name = "Test Project MCP Delete Me"
254+
# result_create = await client.call_tool(
255+
# "create_sonarqube_project",
256+
# {"project_key": new_project_key, "project_name": new_project_name, "visibility": "private"},
257+
# )
258+
# print(f"Create project result: {result_create}")
259+
260+
# except ClientError as e:
261+
# print(f"\n--- MCP Client Error during project creation ---")
262+
# print(f"Error: {e}")
263+
# except Exception as e:
264+
# print(f"\n--- An Unexpected Error Occurred during project creation---")
265+
# print(f"Error type: {type(e).__name__}")
266+
# print(f"Error details: {e}")
267+
268+
269+
# try:
270+
# async with client:
271+
# print("\n--- Testing delete_sonarqube_project (USE WITH CAUTION) ---")
272+
# project_to_delete = new_project_key
273+
# result_delete = await client.call_tool(
274+
# "delete_sonarqube_project", {"project_key": project_to_delete}
275+
# )
276+
# print(f"Delete project result: {result_delete}")
277+
278+
# except ClientError as e:
279+
# print(f"\n--- MCP Client Error during project deletion---")
280+
# print(f"Error: {e}")
281+
# except Exception as e:
282+
# print(f"\n--- An Unexpected Error Occurred during project deletion---")
283+
# print(f"Error type: {type(e).__name__}")
284+
# print(f"Error details: {e}")
246285

247286
print("\n--- Test Complete ---")
248287

server.py

Lines changed: 113 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
from pydantic import Field
88
from fastmcp import FastMCP
99
import logging
10-
from mcp.server import Server
1110
from fastmcp.utilities.logging import get_logger
1211

1312
logger = get_logger(__name__)
@@ -38,6 +37,18 @@
3837
sonarqube_token = os.environ.get("SONARQUBE_TOKEN")
3938
sonarqube_url = os.environ.get("SONARQUBE_URL")
4039

40+
def get_auth_headers():
41+
"""Helper function to get the authorization headers."""
42+
if not sonarqube_token:
43+
raise ValueError("SONARQUBE_TOKEN environment variable is not set.")
44+
base64_token = base64.b64encode(f"{sonarqube_token}:".encode()).decode("utf-8")
45+
return {
46+
"Accept": "application/json",
47+
"Authorization": f"Basic {base64_token}"
48+
}
49+
50+
51+
4152
@mcp.tool()
4253
async def get_status() -> str:
4354
"""
@@ -94,6 +105,107 @@ async def get_status() -> str:
94105
logger.error(f"Unexpected error during health check: {e}", exc_info=True)
95106
return f"Unexpected error: {str(e)}"
96107

108+
@mcp.tool()
109+
async def create_sonarqube_project(
110+
project_key: Annotated[str, Field(description="The unique key for the new SonarQube project (e.g., 'my-new-project'). Must be unique.")],
111+
project_name: Annotated[str, Field(description="The name of the new SonarQube project.")],
112+
visibility: Annotated[str, Field(description="Project visibility (public or private). Default: private.", enum=["public", "private"])] = "private"
113+
) -> str:
114+
"""
115+
Creates a new SonarQube project. Requires administrator privileges.
116+
"""
117+
logger.info(f"Creating SonarQube project: key={project_key}, name={project_name}, visibility={visibility}")
118+
119+
headers = get_auth_headers()
120+
api_url = f"{sonarqube_url}/api/projects/create"
121+
params = {
122+
"name": project_name,
123+
"project": project_key,
124+
"visibility": visibility
125+
}
126+
127+
async with httpx.AsyncClient() as client:
128+
try:
129+
response = await client.post(api_url, headers=headers, params=params, timeout=10)
130+
response.raise_for_status() # Raise HTTPStatusError for bad responses
131+
132+
logger.info(f"Project '{project_key}' created successfully.")
133+
return f"Project '{project_name}' (key: '{project_key}') created successfully."
134+
135+
except httpx.HTTPStatusError as e:
136+
if e.response.status_code == 400:
137+
error_detail = e.response.json().get("errors", [{}])[0].get("msg", "Unknown error")
138+
logger.error(f"Failed to create project (400): {error_detail}")
139+
raise ValueError(f"Failed to create project. Error: {error_detail}") from e
140+
elif e.response.status_code == 401:
141+
logger.error("Authentication failed (401). Check token permissions.")
142+
raise PermissionError("Authentication failed. Check your token's permissions (must be admin).") from e
143+
elif e.response.status_code == 403:
144+
logger.error("Access denied (403). Insufficient permissions.")
145+
raise PermissionError("Access denied. Your token lacks the necessary permissions (must be admin).") from e
146+
else:
147+
logger.error(f"SonarQube API error creating project: {e.response.status_code} - {e.response.text}")
148+
raise RuntimeError(f"SonarQube API error: {e.response.status_code} - {e.response.text}") from e
149+
except httpx.RequestError as e:
150+
logger.error(f"Network error connecting to SonarQube: {e}")
151+
raise ConnectionError(f"Could not connect to SonarQube at {sonarqube_url}") from e
152+
except json.JSONDecodeError as e:
153+
logger.error(f"Failed to decode JSON response: {e}. Response text: {e.response.text if 'e.response' in locals() else 'No response'}")
154+
raise ValueError("Received invalid JSON response from SonarQube.") from e
155+
except Exception as e:
156+
logger.error(f"Unexpected error creating project: {e}", exc_info=True)
157+
raise RuntimeError("An unexpected error occurred.") from e
158+
159+
@mcp.tool()
160+
async def delete_sonarqube_project(
161+
project_key: Annotated[str, Field(description="The unique key of the SonarQube project to delete (e.g., 'my-project-key'). Requires administrator privileges.")],
162+
) -> str:
163+
"""
164+
Deletes a SonarQube project. Requires administrator privileges. USE WITH CAUTION!
165+
"""
166+
logger.warning(f"Attempting to delete SonarQube project: {project_key}") # Use warning, to make it visible
167+
168+
headers = get_auth_headers()
169+
api_url = f"{sonarqube_url}/api/projects/delete"
170+
params = {
171+
"key": project_key
172+
}
173+
174+
async with httpx.AsyncClient() as client:
175+
try:
176+
response = await client.post(api_url, headers=headers, params=params, timeout=10)
177+
response.raise_for_status()
178+
179+
logger.warning(f"Project '{project_key}' has been deleted.") # Also warning, because it's destructive
180+
return f"Project '{project_key}' has been deleted."
181+
182+
except httpx.HTTPStatusError as e:
183+
if e.response.status_code == 400:
184+
error_detail = e.response.json().get("errors", [{}])[0].get("msg", "Unknown error")
185+
logger.error(f"Failed to delete project (400): {error_detail}")
186+
raise ValueError(f"Failed to delete project. Error: {error_detail}") from e
187+
elif e.response.status_code == 401:
188+
logger.error("Authentication failed (401). Check token permissions.")
189+
raise PermissionError("Authentication failed. Check your token's permissions (must be admin).") from e
190+
elif e.response.status_code == 403:
191+
logger.error("Access denied (403). Insufficient permissions.")
192+
raise PermissionError("Access denied. Your token lacks the necessary permissions (must be admin).") from e
193+
elif e.response.status_code == 404:
194+
logger.error(f"Project '{project_key}' not found (404).")
195+
raise ValueError(f"Project '{project_key}' not found.") from e
196+
else:
197+
logger.error(f"SonarQube API error deleting project: {e.response.status_code} - {e.response.text}")
198+
raise RuntimeError(f"SonarQube API error: {e.response.status_code} - {e.response.text}") from e
199+
except httpx.RequestError as e:
200+
logger.error(f"Network error connecting to SonarQube: {e}")
201+
raise ConnectionError(f"Could not connect to SonarQube at {sonarqube_url}") from e
202+
except json.JSONDecodeError as e:
203+
logger.error(f"Failed to decode JSON response: {e}. Response text: {e.response.text if 'e.response' in locals() else 'No response'}")
204+
raise ValueError("Received invalid JSON response from SonarQube.") from e
205+
except Exception as e:
206+
logger.error(f"Unexpected error deleting project: {e}", exc_info=True)
207+
raise RuntimeError("An unexpected error occurred.") from e
208+
97209
@mcp.tool()
98210
async def get_sonarqube_metrics(
99211
project_key: Annotated[str, Field(description="The unique key of the project in SonarQube (e.g., 'my-org_my-repo').")],

0 commit comments

Comments
 (0)