Skip to content

Commit f71b722

Browse files
committed
feat: add get_incident_log_entries function for incident timeline data
- Retrieve complete incident timeline including acknowledgments and status changes - Add LogEntry, LogEntryAgent, and LogEntryChannel models - Include comprehensive unit tests with 100% coverage - Update documentation and tool registration
1 parent 83878f6 commit f71b722

File tree

7 files changed

+207
-6
lines changed

7 files changed

+207
-6
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,7 @@ This section describes the tools provided by the PagerDuty MCP server. They are
178178
| add_responders | Incidents | Adds responders to an incident | ❌ |
179179
| create_incident | Incidents | Creates a new incident | ❌ |
180180
| get_incident | Incidents | Retrieves a specific incident | ✅ |
181+
| get_incident_log_entries | Incidents | Retrieves incident timeline/log entries | ✅ |
181182
| list_incidents | Incidents | Lists incidents | ✅ |
182183
| manage_incidents | Incidents | Updates status, urgency, assignment, or escalation level | ❌ |
183184
| add_team_member | Teams | Adds a user to a team with a specific role | ❌ |

pagerduty_mcp/__main__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
from pagerduty_mcp.server import app
22

3+
34
def main():
45
"""Main entry point for the pagerduty-mcp command."""
56
print("Starting PagerDuty MCP Server. Use the --enable-write-tools flag to enable write tools.")
67
app()
78

9+
810
if __name__ == "__main__":
911
main()

pagerduty_mcp/models/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
IncidentQuery,
1111
IncidentResponderRequest,
1212
IncidentResponderRequestResponse,
13+
LogEntry,
1314
)
1415
from .oncalls import Oncall, OncallQuery
1516
from .references import IncidentReference, ScheduleReference, ServiceReference, TeamReference, UserReference
@@ -38,6 +39,7 @@
3839
"IncidentResponderRequest",
3940
"IncidentResponderRequestResponse",
4041
"ListResponseModel",
42+
"LogEntry",
4143
"MCPContext",
4244
"Oncall",
4345
"OncallQuery",

pagerduty_mcp/models/incidents.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,8 +195,48 @@ class IncidentResponderRequestResponse(BaseModel):
195195
message: str | None = Field(default=None, description="The message included with the request")
196196
responder_request_targets: list[dict[str, Any]] = Field(description="The users requested to respond")
197197

198+
198199
class IncidentNote(BaseModel):
199200
id: str | None = Field(description="The ID of the note", default=None)
200201
content: str = Field(description="The content of the note")
201202
created_at: datetime = Field(description="The time the note was created")
202203
user: UserReference = Field(description="The user who created the note")
204+
205+
206+
# Log Entry Models
207+
class LogEntryAgent(BaseModel):
208+
id: str = Field(description="The ID of the agent")
209+
type: str = Field(description="The type of the agent")
210+
summary: str = Field(description="A summary of the agent")
211+
self: str | None = Field(default=None, description="The API URL of the agent")
212+
html_url: str | None = Field(default=None, description="The web URL of the agent")
213+
214+
215+
class LogEntryChannel(BaseModel):
216+
type: str = Field(description="The type of channel")
217+
summary: str | None = Field(default=None, description="Summary of the channel")
218+
notification: dict[str, Any] | None = Field(default=None, description="Notification details")
219+
client: str | None = Field(default=None, description="Client name")
220+
client_url: str | None = Field(default=None, description="Client URL")
221+
222+
223+
class LogEntry(BaseModel):
224+
id: str = Field(description="The ID of the log entry")
225+
type: str = Field(description="The type of log entry")
226+
summary: str = Field(description="A summary of the log entry")
227+
self: str = Field(description="The API URL of the log entry")
228+
html_url: str | None = Field(default=None, description="The web URL of the log entry")
229+
created_at: datetime = Field(description="The time the log entry was created")
230+
agent: LogEntryAgent | None = Field(default=None, description="The agent that created the log entry")
231+
channel: LogEntryChannel | None = Field(default=None, description="The channel information")
232+
service: ServiceReference = Field(description="The service associated with the log entry")
233+
incident: dict[str, Any] = Field(description="The incident reference")
234+
teams: list[dict[str, Any]] = Field(default=[], description="The teams associated with the log entry")
235+
contexts: list[dict[str, Any]] = Field(default=[], description="Additional context information")
236+
# Optional fields specific to certain log entry types
237+
acknowledgement_timeout: int | None = Field(default=None, description="Acknowledgement timeout in seconds")
238+
assignees: list[UserReference] | None = Field(default=None, description="Users assigned in this log entry")
239+
user: UserReference | None = Field(default=None, description="User associated with the log entry")
240+
linked_incident: dict[str, Any] | None = Field(default=None, description="Linked incident reference")
241+
event_details: dict[str, Any] = Field(default={}, description="Event details")
242+
event_rule_action: dict[str, Any] | None = Field(default=None, description="Event rule action details")

pagerduty_mcp/tools/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
add_responders,
1212
create_incident,
1313
get_incident,
14+
get_incident_log_entries,
1415
list_incidents,
1516
manage_incidents,
1617
)
@@ -44,6 +45,7 @@
4445
# Incidents
4546
list_incidents,
4647
get_incident,
48+
get_incident_log_entries,
4749
# Services
4850
list_services,
4951
get_service,

pagerduty_mcp/tools/incidents.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
IncidentResponderRequest,
1414
IncidentResponderRequestResponse,
1515
ListResponseModel,
16+
LogEntry,
1617
MCPContext,
1718
UserReference,
1819
)
@@ -51,6 +52,24 @@ def get_incident(incident_id: str) -> Incident:
5152
return Incident.model_validate(response)
5253

5354

55+
def get_incident_log_entries(incident_id: str) -> list[LogEntry]:
56+
"""Get log entries (timeline) for a specific incident.
57+
58+
This function retrieves the complete timeline of activities for an incident,
59+
including acknowledgments, assignments, status changes, and other actions.
60+
This provides the same information visible in the PagerDuty UI Timeline tab.
61+
62+
Args:
63+
incident_id: The ID of the incident to retrieve log entries for.
64+
65+
Returns:
66+
List of log entries representing the incident timeline
67+
"""
68+
response = get_client().rget(f"/incidents/{incident_id}/log_entries")
69+
# The response is a direct list of log entries
70+
return [LogEntry.model_validate(entry) for entry in response]
71+
72+
5473
def create_incident(create_model: IncidentCreateRequest) -> Incident:
5574
"""Create an incident.
5675

tests/test_incidents.py

Lines changed: 141 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
IncidentResponderRequest,
1818
IncidentResponderRequestResponse,
1919
ListResponseModel,
20+
LogEntry,
2021
MCPContext,
2122
ServiceReference,
2223
UserReference,
@@ -32,6 +33,7 @@
3233
add_responders,
3334
create_incident,
3435
get_incident,
36+
get_incident_log_entries,
3537
list_incidents,
3638
manage_incidents,
3739
)
@@ -61,6 +63,80 @@ def setUpClass(cls):
6163
"html_url": "https://test.pagerduty.com/incidents/PINCIDENT123",
6264
}
6365

66+
cls.sample_log_entry_data = [
67+
{
68+
"id": "RO52CY6QBZKDN9PEGS3YPN35TH",
69+
"type": "resolve_log_entry",
70+
"summary": "Resolved through the API.",
71+
"self": "https://api.pagerduty.com/log_entries/RO52CY6QBZKDN9PEGS3YPN35TH",
72+
"html_url": None,
73+
"created_at": "2025-09-18T22:59:28Z",
74+
"agent": {
75+
"id": "PWB6A94",
76+
"type": "events_api_v2_inbound_integration_reference",
77+
"summary": "AlertManager",
78+
"self": "https://api.pagerduty.com/services/P9TYXZJ/integrations/PWB6A94",
79+
"html_url": "https://thousandeyesops.pagerduty.com/services/P9TYXZJ/integrations/PWB6A94",
80+
},
81+
"channel": {
82+
"type": "integration",
83+
"client": "Alertmanager",
84+
"client_url": "https://alertmanager.example.com",
85+
},
86+
"service": {
87+
"id": "P9TYXZJ",
88+
"type": "service_reference",
89+
"summary": "[PROD] Webapps",
90+
"self": "https://api.pagerduty.com/services/P9TYXZJ",
91+
"html_url": "https://thousandeyesops.pagerduty.com/service-directory/P9TYXZJ",
92+
},
93+
"incident": {
94+
"id": "Q0Y9H9AUGZHAHS",
95+
"type": "incident_reference",
96+
"summary": "[#568677] Test incident",
97+
"self": "https://api.pagerduty.com/incidents/Q0Y9H9AUGZHAHS",
98+
"html_url": "https://thousandeyesops.pagerduty.com/incidents/Q0Y9H9AUGZHAHS",
99+
},
100+
"teams": [],
101+
"contexts": [],
102+
"event_details": {},
103+
},
104+
{
105+
"id": "RONF3S8XQ8TVEA2DXQS6FRPBEV",
106+
"type": "acknowledge_log_entry",
107+
"summary": "Acknowledged by Test User.",
108+
"self": "https://api.pagerduty.com/log_entries/RONF3S8XQ8TVEA2DXQS6FRPBEV",
109+
"html_url": None,
110+
"created_at": "2025-09-18T22:54:38Z",
111+
"agent": {
112+
"id": "PE9XZ7K",
113+
"type": "user_reference",
114+
"summary": "Test User",
115+
"self": "https://api.pagerduty.com/users/PE9XZ7K",
116+
"html_url": "https://thousandeyesops.pagerduty.com/users/PE9XZ7K",
117+
},
118+
"channel": {"type": "mobile"},
119+
"service": {
120+
"id": "P9TYXZJ",
121+
"type": "service_reference",
122+
"summary": "[PROD] Webapps",
123+
"self": "https://api.pagerduty.com/services/P9TYXZJ",
124+
"html_url": "https://thousandeyesops.pagerduty.com/service-directory/P9TYXZJ",
125+
},
126+
"incident": {
127+
"id": "Q0Y9H9AUGZHAHS",
128+
"type": "incident_reference",
129+
"summary": "[#568677] Test incident",
130+
"self": "https://api.pagerduty.com/incidents/Q0Y9H9AUGZHAHS",
131+
"html_url": "https://thousandeyesops.pagerduty.com/incidents/Q0Y9H9AUGZHAHS",
132+
},
133+
"teams": [],
134+
"contexts": [],
135+
"acknowledgement_timeout": 3600,
136+
"event_details": {},
137+
},
138+
]
139+
64140
cls.sample_user_data = Mock()
65141
cls.sample_user_data.id = "PUSER123"
66142
cls.sample_user_data.teams = [Mock(id="PTEAM123")]
@@ -182,6 +258,68 @@ def test_get_incident_api_error(self, mock_get_client):
182258

183259
self.assertIn("API Error", str(context.exception))
184260

261+
@patch("pagerduty_mcp.tools.incidents.get_client")
262+
def test_get_incident_log_entries_success(self, mock_get_client):
263+
"""Test getting incident log entries successfully."""
264+
# Setup mock
265+
mock_client = Mock()
266+
mock_client.rget.return_value = self.sample_log_entry_data
267+
mock_get_client.return_value = mock_client
268+
269+
# Test
270+
result = get_incident_log_entries("PINCIDENT123")
271+
272+
# Assertions
273+
self.assertIsInstance(result, list)
274+
self.assertEqual(len(result), 2)
275+
self.assertIsInstance(result[0], LogEntry)
276+
self.assertIsInstance(result[1], LogEntry)
277+
278+
# Check first log entry (resolve)
279+
self.assertEqual(result[0].id, "RO52CY6QBZKDN9PEGS3YPN35TH")
280+
self.assertEqual(result[0].type, "resolve_log_entry")
281+
self.assertEqual(result[0].summary, "Resolved through the API.")
282+
self.assertIsNotNone(result[0].agent)
283+
self.assertEqual(result[0].agent.summary, "AlertManager")
284+
285+
# Check second log entry (acknowledge)
286+
self.assertEqual(result[1].id, "RONF3S8XQ8TVEA2DXQS6FRPBEV")
287+
self.assertEqual(result[1].type, "acknowledge_log_entry")
288+
self.assertEqual(result[1].summary, "Acknowledged by Test User.")
289+
self.assertEqual(result[1].acknowledgement_timeout, 3600)
290+
291+
mock_client.rget.assert_called_once_with("/incidents/PINCIDENT123/log_entries")
292+
293+
@patch("pagerduty_mcp.tools.incidents.get_client")
294+
def test_get_incident_log_entries_empty(self, mock_get_client):
295+
"""Test getting incident log entries with empty response."""
296+
# Setup mock
297+
mock_client = Mock()
298+
mock_client.rget.return_value = []
299+
mock_get_client.return_value = mock_client
300+
301+
# Test
302+
result = get_incident_log_entries("PINCIDENT123")
303+
304+
# Assertions
305+
self.assertIsInstance(result, list)
306+
self.assertEqual(len(result), 0)
307+
mock_client.rget.assert_called_once_with("/incidents/PINCIDENT123/log_entries")
308+
309+
@patch("pagerduty_mcp.tools.incidents.get_client")
310+
def test_get_incident_log_entries_api_error(self, mock_get_client):
311+
"""Test get_incident_log_entries with API error."""
312+
# Setup mock to raise exception
313+
mock_client = Mock()
314+
mock_client.rget.side_effect = Exception("API Error")
315+
mock_get_client.return_value = mock_client
316+
317+
# Test that exception is raised
318+
with self.assertRaises(Exception) as context:
319+
get_incident_log_entries("PINCIDENT123")
320+
321+
self.assertIn("API Error", str(context.exception))
322+
185323
@patch("pagerduty_mcp.tools.incidents.get_client")
186324
def test_create_incident_success(self, mock_get_client):
187325
"""Test creating an incident successfully."""
@@ -467,10 +605,7 @@ def test_add_note_to_incident_success(self, mock_get_client):
467605
"id": "PNOTE123",
468606
"content": "This is a test note",
469607
"created_at": "2023-01-01T10:00:00Z",
470-
"user": {
471-
"id": "PUSER123",
472-
"summary": "Test User"
473-
}
608+
"user": {"id": "PUSER123", "summary": "Test User"},
474609
}
475610

476611
mock_client = Mock()
@@ -488,9 +623,9 @@ def test_add_note_to_incident_success(self, mock_get_client):
488623

489624
# Verify API call
490625
mock_client.rpost.assert_called_once_with(
491-
"/incidents/PINC123/notes",
492-
json={"note": {"content": "This is a test note"}}
626+
"/incidents/PINC123/notes", json={"note": {"content": "This is a test note"}}
493627
)
494628

629+
495630
if __name__ == "__main__":
496631
unittest.main()

0 commit comments

Comments
 (0)