Skip to content

Commit c8e4de9

Browse files
committed
fix(list-incidents): Fix the tool and make it more generic
1 parent 5252498 commit c8e4de9

File tree

8 files changed

+222
-358
lines changed

8 files changed

+222
-358
lines changed

packages/developer_mcp_server/src/developer_mcp_server/register_tools.py

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from gg_api_core.tools.find_current_source_id import find_current_source_id
33
from gg_api_core.tools.generate_honey_token import generate_honeytoken
44
from gg_api_core.tools.list_honey_tokens import list_honeytokens
5-
from gg_api_core.tools.list_repo_incidents import list_repo_incidents
5+
from gg_api_core.tools.list_incidents import list_incidents
66
from gg_api_core.tools.list_repo_occurrences import list_repo_occurrences
77
from gg_api_core.tools.list_users import list_users
88
from gg_api_core.tools.remediate_secret_incidents import remediate_secret_incidents
@@ -19,7 +19,7 @@
1919
2020
1. **Finding Existing Secret Incidents**:
2121
- Detect secrets already identified as GitGuardian incidents in your repository
22-
- Use `list_repo_incidents` to view all secret incidents in a repository
22+
- Use `list_incidents` to view all secret incidents in a repository
2323
- Filter incidents by various criteria including those assigned to you
2424
2525
2. **Proactive Secret Scanning**:
@@ -67,10 +67,9 @@ def register_developer_tools(mcp: AbstractGitGuardianFastMCP):
6767
)
6868

6969
mcp.tool(
70-
list_repo_incidents,
71-
description="List secret incidents or occurrences related to a specific repository, and assigned to the current user."
72-
"By default, this tool only shows incidents assigned to the current user. "
73-
"Only pass mine=False to get all incidents related to this repo if the user explicitly asks for all incidents even the ones not assigned to him.",
70+
list_incidents,
71+
description="List secret incidents or occurrences related to a specific repository"
72+
"With mine=True, this tool only shows incidents assigned to the current user.",
7473
required_scopes=["incidents:read", "sources:read"],
7574
)
7675

packages/gg_api_core/src/gg_api_core/client.py

Lines changed: 34 additions & 146 deletions
Original file line numberDiff line numberDiff line change
@@ -749,56 +749,37 @@ async def list_incidents(
749749

750750
# Process severity parameter
751751
if severity:
752-
# If it's already an enum, use its value
752+
# If it's an enum, get its value
753753
if isinstance(severity, IncidentSeverity):
754754
params["severity"] = severity.value
755-
# If it's a string, validate and convert
755+
# If it's a string, pass it through directly
756+
# The API will handle validation and support comma-separated values
756757
elif isinstance(severity, str):
757-
try:
758-
# Convert to enum to validate, then get the value
759-
params["severity"] = IncidentSeverity(severity.lower()).value
760-
except ValueError:
761-
valid_values = [e.value for e in IncidentSeverity]
762-
logger.warning(f"Invalid severity value: {severity}. Must be one of {valid_values}")
763-
raise ValueError(f"Invalid severity value: {severity}. Must be one of {valid_values}")
758+
params["severity"] = severity
764759
else:
765760
raise TypeError("severity must be a string or IncidentSeverity enum")
766761

767762
# Process status parameter
768763
if status:
769-
# If it's already an enum, use its value
764+
# If it's an enum, get its value
770765
if isinstance(status, IncidentStatus):
771766
params["status"] = status.value
772-
# If it's a string, validate and convert
767+
# If it's a string, pass it through directly
768+
# The API will handle validation and support comma-separated values
773769
elif isinstance(status, str):
774-
try:
775-
# For status, we need to check if it's uppercase already
776-
if status in [e.value for e in IncidentStatus]:
777-
params["status"] = status
778-
else:
779-
# Try with uppercase for compatibility
780-
params["status"] = IncidentStatus(status.upper()).value
781-
except ValueError:
782-
valid_values = [e.value for e in IncidentStatus]
783-
logger.warning(f"Invalid status value: {status}. Must be one of {valid_values}")
784-
raise ValueError(f"Invalid status value: {status}. Must be one of {valid_values}")
770+
params["status"] = status
785771
else:
786772
raise TypeError("status must be a string or IncidentStatus enum")
787773

788774
# Process validity parameter
789775
if validity:
790-
# If it's already an enum, use its value
776+
# If it's an enum, get its value
791777
if isinstance(validity, IncidentValidity):
792778
params["validity"] = validity.value
793-
# If it's a string, validate and convert
779+
# If it's a string, pass it through directly
780+
# The API will handle validation and support comma-separated values
794781
elif isinstance(validity, str):
795-
try:
796-
# Convert to enum to validate, then get the value
797-
params["validity"] = IncidentValidity(validity.lower()).value
798-
except ValueError:
799-
valid_values = [e.value for e in IncidentValidity]
800-
logger.warning(f"Invalid validity value: {validity}. Must be one of {valid_values}")
801-
raise ValueError(f"Invalid validity value: {validity}. Must be one of {valid_values}")
782+
params["validity"] = validity
802783
else:
803784
raise TypeError("validity must be a string or IncidentValidity enum")
804785

@@ -987,7 +968,13 @@ async def get_current_token_info(self) -> dict[str, Any]:
987968

988969
# If we already have token info, return it
989970
if self._token_info is not None:
990-
return self._token_info
971+
# Convert Pydantic model to dict if needed
972+
if hasattr(self._token_info, "model_dump"):
973+
return self._token_info.model_dump()
974+
elif isinstance(self._token_info, dict):
975+
return self._token_info
976+
else:
977+
return await self._request("GET", "/api_tokens/self")
991978

992979
# Otherwise fetch from the API
993980
return await self._request("GET", "/api_tokens/self")
@@ -1579,119 +1566,6 @@ async def get_source_by_name(
15791566
logger.error(f"Error getting source by name: {str(e)}")
15801567
return None
15811568

1582-
async def list_repo_incidents_directly(
1583-
self,
1584-
repository_name: str,
1585-
from_date: str | None = None,
1586-
to_date: str | None = None,
1587-
presence: str | None = None,
1588-
tags: list[str] | None = None,
1589-
exclude_tags: list[str] | None = None,
1590-
per_page: int = 20,
1591-
cursor: str | None = None,
1592-
ordering: str | None = None,
1593-
get_all: bool = False,
1594-
mine: bool = True,
1595-
) -> dict[str, Any]:
1596-
"""List incidents for a repository in a single API call.
1597-
1598-
This method offers better performance than the two-step process of
1599-
getting occurrences first and then incidents separately.
1600-
1601-
Args:
1602-
repository_name: Name of the repository
1603-
from_date: Filter incidents created after this date (ISO format)
1604-
to_date: Filter incidents created before this date (ISO format)
1605-
presence: Filter by presence status
1606-
tags: Filter by tags
1607-
exclude_tags: Exclude incidents with these tag names
1608-
per_page: Number of results per page
1609-
cursor: Pagination cursor
1610-
ordering: Sort field
1611-
get_all: Whether to fetch all results using pagination
1612-
mine: If True, only show incidents assigned to the current user
1613-
1614-
Returns:
1615-
Dictionary containing the incidents and pagination info
1616-
"""
1617-
logger.info(f"Directly listing incidents for repository: {repository_name}")
1618-
1619-
# First get the source ID for the repository name
1620-
source = await self.get_source_by_name(repository_name)
1621-
1622-
if not source:
1623-
return {
1624-
"repository_info": {"name": repository_name},
1625-
"incidents": [],
1626-
"message": f"Repository '{repository_name}' not found or not accessible",
1627-
}
1628-
1629-
source_id = source.get("id")
1630-
logger.info(f"Found source ID {source_id} for repository {repository_name}")
1631-
1632-
# Prepare parameters for the API call
1633-
params = {}
1634-
if from_date:
1635-
params["from_date"] = from_date
1636-
if to_date:
1637-
params["to_date"] = to_date
1638-
if presence:
1639-
params["presence"] = presence
1640-
if tags:
1641-
params["tags"] = ",".join(tags) if isinstance(tags, list) else tags
1642-
if exclude_tags:
1643-
params["exclude_tags"] = ",".join(exclude_tags) if isinstance(exclude_tags, list) else exclude_tags
1644-
if per_page:
1645-
params["per_page"] = per_page
1646-
if cursor:
1647-
params["cursor"] = cursor
1648-
if ordering:
1649-
params["ordering"] = ordering
1650-
if mine:
1651-
params["assigned_to_me"] = "true"
1652-
1653-
try:
1654-
if get_all:
1655-
# Use pagination to get all results
1656-
incidents_result = await self.paginate_all(f"/sources/{source_id}/incidents/secrets", params)
1657-
if isinstance(incidents_result, list):
1658-
return {
1659-
"repository_info": source,
1660-
"incidents": incidents_result,
1661-
"total_count": len(incidents_result),
1662-
}
1663-
# If it's already a dict with structure, return it
1664-
return incidents_result
1665-
1666-
# Get a single page of results
1667-
incidents_result = await self.list_source_incidents(source_id, **params)
1668-
1669-
# Make sure incidents_result is a dictionary before using .get()
1670-
if isinstance(incidents_result, dict):
1671-
return {
1672-
"repository_info": source,
1673-
"incidents": incidents_result.get("data", []),
1674-
"next_cursor": incidents_result.get("next_cursor"),
1675-
"total_count": incidents_result.get("total_count", 0),
1676-
}
1677-
elif isinstance(incidents_result, list):
1678-
return {
1679-
"repository_info": source,
1680-
"incidents": incidents_result,
1681-
"total_count": len(incidents_result),
1682-
}
1683-
else:
1684-
return {
1685-
"repository_info": source,
1686-
"incidents": [],
1687-
"total_count": 0,
1688-
"error": "Unexpected response format from API",
1689-
}
1690-
1691-
except Exception as e:
1692-
logger.error(f"Error listing repository incidents directly: {str(e)}")
1693-
return {"error": f"Failed to list repository incidents: {str(e)}"}
1694-
16951569
async def create_code_fix_request(self, locations: list[dict[str, Any]]) -> dict[str, Any]:
16961570
"""Create code fix requests for multiple secret incidents with their locations.
16971571
@@ -1714,4 +1588,18 @@ async def create_code_fix_request(self, locations: list[dict[str, Any]]) -> dict
17141588
404: API key not configured)
17151589
"""
17161590
logger.info(f"Creating code fix request for {len(locations)} issue(s)")
1717-
return await self._request("POST", "/v1/code-fix-requests", json={"locations": locations})
1591+
return await self._request("POST", "/code-fix-requests", json={"locations": locations})
1592+
1593+
async def list_members(self, params):
1594+
"""List all users in the account."""
1595+
return await self._request("GET", "/members", params=params, return_headers=True)
1596+
1597+
async def get_member(self, member_id):
1598+
"""Get a specific user's information."""
1599+
return await self._request("GET", f"/members/{member_id}")
1600+
1601+
async def get_current_member(self):
1602+
"""Get the current user's information."""
1603+
data = await self.get_current_token_info()
1604+
member_id = data["member_id"]
1605+
return await self.get_member(member_id)

0 commit comments

Comments
 (0)