|
7 | 7 | from pydantic import Field |
8 | 8 | from fastmcp import FastMCP |
9 | 9 | import logging |
10 | | -from mcp.server import Server |
11 | 10 | from fastmcp.utilities.logging import get_logger |
12 | 11 |
|
13 | 12 | logger = get_logger(__name__) |
|
38 | 37 | sonarqube_token = os.environ.get("SONARQUBE_TOKEN") |
39 | 38 | sonarqube_url = os.environ.get("SONARQUBE_URL") |
40 | 39 |
|
| 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 | + |
41 | 52 | @mcp.tool() |
42 | 53 | async def get_status() -> str: |
43 | 54 | """ |
@@ -94,6 +105,107 @@ async def get_status() -> str: |
94 | 105 | logger.error(f"Unexpected error during health check: {e}", exc_info=True) |
95 | 106 | return f"Unexpected error: {str(e)}" |
96 | 107 |
|
| 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 | + |
97 | 209 | @mcp.tool() |
98 | 210 | async def get_sonarqube_metrics( |
99 | 211 | project_key: Annotated[str, Field(description="The unique key of the project in SonarQube (e.g., 'my-org_my-repo').")], |
|
0 commit comments