Skip to content

Commit bf4218e

Browse files
committed
chore(tools): Extract one tool as a file
1 parent 88d9246 commit bf4218e

File tree

4 files changed

+148
-138
lines changed

4 files changed

+148
-138
lines changed

packages/developer_mcp_server/src/developer_mcp_server/server.py

Lines changed: 3 additions & 136 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
from gg_api_core.mcp_server import GitGuardianFastMCP
99
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
1011
from gg_api_core.utils import parse_repo_url
1112

1213
from gg_api_core.tools.list_repo_incidents import list_repo_incidents
@@ -129,147 +130,13 @@
129130
)
130131

131132

132-
@mcp.tool(
133+
mcp.add_tool(
134+
find_current_source_id,
133135
description="Find the GitGuardian source_id for the current repository. "
134136
"This tool automatically detects the current git repository and searches for its source_id in GitGuardian. "
135137
"Useful when you need to reference the repository in other API calls.",
136138
required_scopes=["sources:read"],
137139
)
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-
273140

274141
# TODO(APPAI-28)
275142
# mcp.add_tool(
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
from typing import Any
2+
import logging
3+
import subprocess
4+
from gg_api_core.utils import get_client, parse_repo_url
5+
6+
logger = logging.getLogger(__name__)
7+
8+
9+
10+
async def find_current_source_id() -> dict[str, Any]:
11+
"""
12+
Find the GitGuardian source_id for the current repository.
13+
14+
This tool:
15+
1. Gets the current repository information from git
16+
2. Extracts the repository name from the remote URL
17+
3. Searches GitGuardian for matching sources
18+
4. Returns the source_id if an exact match is found
19+
5. If no exact match, returns all search results for the model to choose from
20+
21+
Returns:
22+
A dictionary containing:
23+
- repository_name: The detected repository name
24+
- source_id: The GitGuardian source ID (if exact match found)
25+
- source: Full source information from GitGuardian (if exact match found)
26+
- candidates: List of candidate sources (if no exact match but potential matches found)
27+
- error: Error message if something went wrong
28+
"""
29+
client = get_client()
30+
logger.debug("Finding source_id for current repository")
31+
32+
try:
33+
# Get current repository remote URL
34+
try:
35+
result = subprocess.run(
36+
["git", "config", "--get", "remote.origin.url"],
37+
capture_output=True,
38+
text=True,
39+
check=True,
40+
timeout=5,
41+
)
42+
remote_url = result.stdout.strip()
43+
logger.debug(f"Found remote URL: {remote_url}")
44+
except subprocess.CalledProcessError as e:
45+
return {
46+
"error": "Not a git repository or no remote 'origin' configured",
47+
"details": str(e),
48+
}
49+
except subprocess.TimeoutExpired:
50+
return {"error": "Git command timed out"}
51+
52+
# Parse repository name from remote URL
53+
repository_name = parse_repo_url(remote_url)
54+
55+
if not repository_name:
56+
return {
57+
"error": f"Could not parse repository URL: {remote_url}",
58+
"details": "The URL format is not recognized. Supported platforms: GitHub, GitLab (Cloud & Self-hosted), Bitbucket (Cloud & Data Center), Azure DevOps",
59+
}
60+
61+
logger.info(f"Detected repository name: {repository_name}")
62+
63+
# Search for the source in GitGuardian with robust non-exact matching
64+
result = await client.get_source_by_name(repository_name, return_all_on_no_match=True)
65+
66+
# Handle exact match (single dict result)
67+
if isinstance(result, dict):
68+
source_id = result.get("id")
69+
logger.info(f"Found exact match with source_id: {source_id}")
70+
return {
71+
"repository_name": repository_name,
72+
"source_id": source_id,
73+
"source": result,
74+
"message": f"Successfully found exact match for GitGuardian source: {repository_name}",
75+
}
76+
77+
# Handle multiple candidates (list result)
78+
elif isinstance(result, list) and len(result) > 0:
79+
logger.info(f"Found {len(result)} candidate sources for repository: {repository_name}")
80+
return {
81+
"repository_name": repository_name,
82+
"message": f"No exact match found for '{repository_name}', but found {len(result)} potential matches.",
83+
"suggestion": "Review the candidates below and determine which source best matches the current repository based on the name and URL.",
84+
"candidates": [
85+
{
86+
"id": source.get("id"),
87+
"url": source.get("url"),
88+
"name": source.get("full_name") or source.get("name"),
89+
"monitored": source.get("monitored"),
90+
"deleted_at": source.get("deleted_at"),
91+
}
92+
for source in result
93+
],
94+
}
95+
96+
# No matches found at all
97+
else:
98+
# Try searching with just the repo name (without org) as fallback
99+
if "/" in repository_name:
100+
repo_only = repository_name.split("/")[-1]
101+
logger.debug(f"Trying fallback search with repo name only: {repo_only}")
102+
fallback_result = await client.get_source_by_name(repo_only, return_all_on_no_match=True)
103+
104+
# Handle fallback results
105+
if isinstance(fallback_result, dict):
106+
source_id = fallback_result.get("id")
107+
logger.info(f"Found match using repo name only, source_id: {source_id}")
108+
return {
109+
"repository_name": repository_name,
110+
"source_id": source_id,
111+
"source": fallback_result,
112+
"message": f"Found match using repository name '{repo_only}' (without organization prefix)",
113+
}
114+
elif isinstance(fallback_result, list) and len(fallback_result) > 0:
115+
logger.info(f"Found {len(fallback_result)} candidates using repo name only")
116+
return {
117+
"repository_name": repository_name,
118+
"message": f"No exact match for '{repository_name}', but found {len(fallback_result)} potential matches using repo name '{repo_only}'.",
119+
"suggestion": "Review the candidates below and determine which source best matches the current repository.",
120+
"candidates": [
121+
{
122+
"id": source.get("id"),
123+
"url": source.get("url"),
124+
"name": source.get("full_name") or source.get("name"),
125+
"monitored": source.get("monitored"),
126+
"deleted_at": source.get("deleted_at"),
127+
}
128+
for source in fallback_result
129+
],
130+
}
131+
132+
# Absolutely no matches found
133+
logger.warning(f"No sources found for repository: {repository_name}")
134+
return {
135+
"repository_name": repository_name,
136+
"error": f"Repository '{repository_name}' not found in GitGuardian",
137+
"message": "The repository may not be connected to GitGuardian, or you may not have access to it.",
138+
"suggestion": "Check that the repository is properly connected to GitGuardian and that your account has access to it.",
139+
}
140+
141+
except Exception as e:
142+
logger.error(f"Error finding source_id: {str(e)}")
143+
return {"error": f"Failed to find source_id: {str(e)}"}

packages/gg_api_core/src/gg_api_core/tools/list_repo_incidents.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ async def list_repo_incidents(
1414
),
1515
source_id: str | None = Field(
1616
default=None,
17-
description="The GitGuardian source ID to filter by. Can be obtained using find_current_repo_source_id. If provided, repository_name is not required."
17+
description="The GitGuardian source ID to filter by. Can be obtained using find_current_source_id. If provided, repository_name is not required."
1818
),
1919
from_date: str | None = Field(
2020
default=None, description="Filter occurrences created after this date (ISO format: YYYY-MM-DD)"

packages/gg_api_core/src/gg_api_core/tools/list_repo_occurrences.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ async def list_repo_occurrences(
1515
),
1616
source_id: str | None = Field(
1717
default=None,
18-
description="The GitGuardian source ID to filter by. Can be obtained using find_current_repo_source_id. If provided, repository_name is not required."
18+
description="The GitGuardian source ID to filter by. Can be obtained using find_current_source_id. If provided, repository_name is not required."
1919
),
2020
from_date: str | None = Field(
2121
default=None, description="Filter occurrences created after this date (ISO format: YYYY-MM-DD)"

0 commit comments

Comments
 (0)