|
7 | 7 |
|
8 | 8 | from gg_api_core.mcp_server import GitGuardianFastMCP |
9 | 9 | from gg_api_core.scopes import get_developer_scopes, is_self_hosted_instance, validate_scopes |
| 10 | +from gg_api_core.tools.find_current_source_id import find_current_source_id |
10 | 11 | from gg_api_core.utils import parse_repo_url |
11 | 12 |
|
12 | 13 | from gg_api_core.tools.list_repo_incidents import list_repo_incidents |
|
129 | 130 | ) |
130 | 131 |
|
131 | 132 |
|
132 | | -@mcp.tool( |
| 133 | +mcp.add_tool( |
| 134 | + find_current_source_id, |
133 | 135 | description="Find the GitGuardian source_id for the current repository. " |
134 | 136 | "This tool automatically detects the current git repository and searches for its source_id in GitGuardian. " |
135 | 137 | "Useful when you need to reference the repository in other API calls.", |
136 | 138 | required_scopes=["sources:read"], |
137 | 139 | ) |
138 | | -async def find_current_repo_source_id() -> dict[str, Any]: |
139 | | - """ |
140 | | - Find the GitGuardian source_id for the current repository. |
141 | | -
|
142 | | - This tool: |
143 | | - 1. Gets the current repository information from git |
144 | | - 2. Extracts the repository name from the remote URL |
145 | | - 3. Searches GitGuardian for matching sources |
146 | | - 4. Returns the source_id if an exact match is found |
147 | | - 5. If no exact match, returns all search results for the model to choose from |
148 | | -
|
149 | | - Returns: |
150 | | - A dictionary containing: |
151 | | - - repository_name: The detected repository name |
152 | | - - source_id: The GitGuardian source ID (if exact match found) |
153 | | - - source: Full source information from GitGuardian (if exact match found) |
154 | | - - candidates: List of candidate sources (if no exact match but potential matches found) |
155 | | - - error: Error message if something went wrong |
156 | | - """ |
157 | | - client = mcp.get_client() |
158 | | - logger.debug("Finding source_id for current repository") |
159 | | - |
160 | | - try: |
161 | | - # Get current repository remote URL |
162 | | - try: |
163 | | - result = subprocess.run( |
164 | | - ["git", "config", "--get", "remote.origin.url"], |
165 | | - capture_output=True, |
166 | | - text=True, |
167 | | - check=True, |
168 | | - timeout=5, |
169 | | - ) |
170 | | - remote_url = result.stdout.strip() |
171 | | - logger.debug(f"Found remote URL: {remote_url}") |
172 | | - except subprocess.CalledProcessError as e: |
173 | | - return { |
174 | | - "error": "Not a git repository or no remote 'origin' configured", |
175 | | - "details": str(e), |
176 | | - } |
177 | | - except subprocess.TimeoutExpired: |
178 | | - return {"error": "Git command timed out"} |
179 | | - |
180 | | - # Parse repository name from remote URL |
181 | | - repository_name = parse_repo_url(remote_url) |
182 | | - |
183 | | - if not repository_name: |
184 | | - return { |
185 | | - "error": f"Could not parse repository URL: {remote_url}", |
186 | | - "details": "The URL format is not recognized. Supported platforms: GitHub, GitLab (Cloud & Self-hosted), Bitbucket (Cloud & Data Center), Azure DevOps", |
187 | | - } |
188 | | - |
189 | | - logger.info(f"Detected repository name: {repository_name}") |
190 | | - |
191 | | - # Search for the source in GitGuardian with robust non-exact matching |
192 | | - result = await client.get_source_by_name(repository_name, return_all_on_no_match=True) |
193 | | - |
194 | | - # Handle exact match (single dict result) |
195 | | - if isinstance(result, dict): |
196 | | - source_id = result.get("id") |
197 | | - logger.info(f"Found exact match with source_id: {source_id}") |
198 | | - return { |
199 | | - "repository_name": repository_name, |
200 | | - "source_id": source_id, |
201 | | - "source": result, |
202 | | - "message": f"Successfully found exact match for GitGuardian source: {repository_name}", |
203 | | - } |
204 | | - |
205 | | - # Handle multiple candidates (list result) |
206 | | - elif isinstance(result, list) and len(result) > 0: |
207 | | - logger.info(f"Found {len(result)} candidate sources for repository: {repository_name}") |
208 | | - return { |
209 | | - "repository_name": repository_name, |
210 | | - "message": f"No exact match found for '{repository_name}', but found {len(result)} potential matches.", |
211 | | - "suggestion": "Review the candidates below and determine which source best matches the current repository based on the name and URL.", |
212 | | - "candidates": [ |
213 | | - { |
214 | | - "id": source.get("id"), |
215 | | - "url": source.get("url"), |
216 | | - "name": source.get("full_name") or source.get("name"), |
217 | | - "monitored": source.get("monitored"), |
218 | | - "deleted_at": source.get("deleted_at"), |
219 | | - } |
220 | | - for source in result |
221 | | - ], |
222 | | - } |
223 | | - |
224 | | - # No matches found at all |
225 | | - else: |
226 | | - # Try searching with just the repo name (without org) as fallback |
227 | | - if "/" in repository_name: |
228 | | - repo_only = repository_name.split("/")[-1] |
229 | | - logger.debug(f"Trying fallback search with repo name only: {repo_only}") |
230 | | - fallback_result = await client.get_source_by_name(repo_only, return_all_on_no_match=True) |
231 | | - |
232 | | - # Handle fallback results |
233 | | - if isinstance(fallback_result, dict): |
234 | | - source_id = fallback_result.get("id") |
235 | | - logger.info(f"Found match using repo name only, source_id: {source_id}") |
236 | | - return { |
237 | | - "repository_name": repository_name, |
238 | | - "source_id": source_id, |
239 | | - "source": fallback_result, |
240 | | - "message": f"Found match using repository name '{repo_only}' (without organization prefix)", |
241 | | - } |
242 | | - elif isinstance(fallback_result, list) and len(fallback_result) > 0: |
243 | | - logger.info(f"Found {len(fallback_result)} candidates using repo name only") |
244 | | - return { |
245 | | - "repository_name": repository_name, |
246 | | - "message": f"No exact match for '{repository_name}', but found {len(fallback_result)} potential matches using repo name '{repo_only}'.", |
247 | | - "suggestion": "Review the candidates below and determine which source best matches the current repository.", |
248 | | - "candidates": [ |
249 | | - { |
250 | | - "id": source.get("id"), |
251 | | - "url": source.get("url"), |
252 | | - "name": source.get("full_name") or source.get("name"), |
253 | | - "monitored": source.get("monitored"), |
254 | | - "deleted_at": source.get("deleted_at"), |
255 | | - } |
256 | | - for source in fallback_result |
257 | | - ], |
258 | | - } |
259 | | - |
260 | | - # Absolutely no matches found |
261 | | - logger.warning(f"No sources found for repository: {repository_name}") |
262 | | - return { |
263 | | - "repository_name": repository_name, |
264 | | - "error": f"Repository '{repository_name}' not found in GitGuardian", |
265 | | - "message": "The repository may not be connected to GitGuardian, or you may not have access to it.", |
266 | | - "suggestion": "Check that the repository is properly connected to GitGuardian and that your account has access to it.", |
267 | | - } |
268 | | - |
269 | | - except Exception as e: |
270 | | - logger.error(f"Error finding source_id: {str(e)}") |
271 | | - return {"error": f"Failed to find source_id: {str(e)}"} |
272 | | - |
273 | 140 |
|
274 | 141 | # TODO(APPAI-28) |
275 | 142 | # mcp.add_tool( |
|
0 commit comments