Skip to content

Commit 6199472

Browse files
committed
checkpoint
1 parent ba1bf21 commit 6199472

File tree

7 files changed

+380
-99
lines changed

7 files changed

+380
-99
lines changed

src/oci-monitoring-mcp-server/README.md

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,25 +12,29 @@ uv run oracle.oci-monitoring-mcp-server
1212

1313
## Tools
1414

15-
| Tool Name | Description |
16-
| --- | --- |
17-
| list_metrics | List metrics in the tenancy |
18-
| get_metric | Get metric by name |
15+
| Tool Name | Description |
16+
|-----------------------|------------------------------------------------------------------|
17+
| list_alarms | List Alarms in the tenancy |
18+
| get_metrics_data | Gets aggregated metric data |
19+
| get_available_metrics | Lists the available metrics a user can query on in their tenancy |
1920

20-
21-
⚠️ **NOTE**: All actions are performed with the permissions of the configured OCI CLI profile. We advise least-privilege IAM setup, secure credential management, safe network practices, secure logging, and warn against exposing secrets.
21+
⚠️ **NOTE**: All actions are performed with the permissions of the configured OCI CLI profile. We advise least-privilege
22+
IAM setup, secure credential management, safe network practices, secure logging, and warn against exposing secrets.
2223

2324
## Third-Party APIs
2425

25-
Developers choosing to distribute a binary implementation of this project are responsible for obtaining and providing all required licenses and copyright notices for the third-party code used in order to ensure compliance with their respective open source licenses.
26+
Developers choosing to distribute a binary implementation of this project are responsible for obtaining and providing
27+
all required licenses and copyright notices for the third-party code used in order to ensure compliance with their
28+
respective open source licenses.
2629

2730
## Disclaimer
2831

29-
Users are responsible for their local environment and credential safety. Different language model selections may yield different results and performance.
32+
Users are responsible for their local environment and credential safety. Different language model selections may yield
33+
different results and performance.
3034

3135
## License
3236

3337
Copyright (c) 2025 Oracle and/or its affiliates.
34-
38+
3539
Released under the Universal Permissive License v1.0 as shown at
3640
<https://oss.oracle.com/licenses/upl/>.
Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
"""
2+
Copyright (c) 2025, Oracle and/or its affiliates.
3+
Licensed under the Universal Permissive License v1.0 as shown at
4+
https://oss.oracle.com/licenses/upl.
5+
"""
6+
7+
from datetime import datetime
8+
from typing import Any, Dict, List, Literal, Optional
9+
10+
import oci
11+
from pydantic import BaseModel, Field
12+
13+
14+
def _oci_to_dict(obj):
15+
"""Best-effort conversion of OCI SDK model objects to plain dicts."""
16+
if obj is None:
17+
return None
18+
try:
19+
from oci.util import to_dict as oci_to_dict
20+
21+
return oci_to_dict(obj)
22+
except Exception:
23+
pass
24+
if isinstance(obj, dict):
25+
return obj
26+
if hasattr(obj, "__dict__"):
27+
return {k: v for k, v in obj.__dict__.items() if not k.startswith("_")}
28+
return None
29+
30+
31+
SeverityType = Literal["CRITICAL", "ERROR", "WARNING", "INFO", "UNKNOWN_ENUM_VALUE"]
32+
33+
34+
class Suppression(BaseModel):
35+
"""
36+
Pydantic model mirroring oci.monitoring.models.Suppression.
37+
"""
38+
39+
description: Optional[str] = Field(
40+
None, description="Human-readable description of the suppression."
41+
)
42+
time_suppress_from: Optional[datetime] = Field(
43+
None, description="The start time for the suppression (RFC3339)."
44+
)
45+
time_suppress_until: Optional[datetime] = Field(
46+
None, description="The end time for the suppression (RFC3339)."
47+
)
48+
49+
50+
def map_suppression(s: oci.monitoring.models.Suppression | None) -> Suppression | None:
51+
if not s:
52+
return None
53+
return Suppression(
54+
description=getattr(s, "description", None),
55+
time_suppress_from=getattr(s, "time_suppress_from", None)
56+
or getattr(s, "timeSuppressFrom", None),
57+
time_suppress_until=getattr(s, "time_suppress_until", None)
58+
or getattr(s, "timeSuppressUntil", None),
59+
)
60+
61+
62+
class AlarmOverride(BaseModel):
63+
"""
64+
Pydantic model mirroring (a subset of) oci.monitoring.models.AlarmOverride.
65+
Each override can specify values for query, severity, body, and pending duration.
66+
"""
67+
68+
rule_name: Optional[str] = Field(
69+
None,
70+
description="Identifier of the alarm's base/override values. Default is 'BASE'.",
71+
)
72+
query: Optional[str] = Field(
73+
None, description="MQL expression override for this rule."
74+
)
75+
severity: Optional[SeverityType] = Field(
76+
None, description="Severity override for this rule."
77+
)
78+
body: Optional[str] = Field(None, description="Message body override (alarm body).")
79+
pending_duration: Optional[str] = Field(
80+
None,
81+
description="Override for pending duration as ISO 8601 duration (e.g., 'PT5M').",
82+
)
83+
84+
85+
def map_alarm_override(
86+
o: oci.monitoring.models.AlarmOverride | None,
87+
) -> AlarmOverride | None:
88+
if not o:
89+
return None
90+
return AlarmOverride(
91+
rule_name=getattr(o, "rule_name", None) or getattr(o, "ruleName", None),
92+
query=getattr(o, "query", None),
93+
severity=getattr(o, "severity", None),
94+
body=getattr(o, "body", None),
95+
pending_duration=getattr(o, "pending_duration", None)
96+
or getattr(o, "pendingDuration", None),
97+
)
98+
99+
100+
def map_alarm_overrides(items) -> list[AlarmOverride] | None:
101+
if not items:
102+
return None
103+
result: list[AlarmOverride] = []
104+
for it in items:
105+
mapped = map_alarm_override(it)
106+
if mapped is not None:
107+
result.append(mapped)
108+
return result if result else None
109+
110+
111+
class AlarmSummary(BaseModel):
112+
"""
113+
Pydantic model mirroring (a subset of) oci.monitoring.models.AlarmSummary.
114+
"""
115+
116+
id: Optional[str] = Field(None, description="The OCID of the alarm.")
117+
display_name: Optional[str] = Field(
118+
None,
119+
description="A user-friendly name for the alarm; used as title in notifications.",
120+
)
121+
compartment_id: Optional[str] = Field(
122+
None, description="The OCID of the compartment containing the alarm."
123+
)
124+
metric_compartment_id: Optional[str] = Field(
125+
None,
126+
description="The OCID of the compartment containing the metric evaluated by the alarm.",
127+
)
128+
namespace: Optional[str] = Field(
129+
None, description="The source service/application emitting the metric."
130+
)
131+
query: Optional[str] = Field(
132+
None,
133+
description="The Monitoring Query Language (MQL) expression to evaluate for the alarm.",
134+
)
135+
severity: Optional[SeverityType] = Field(
136+
None,
137+
description="The perceived type of response required when the alarm is FIRING.",
138+
)
139+
destinations: Optional[List[str]] = Field(
140+
None,
141+
description="List of destination OCIDs for alarm notifications (e.g., NotificationTopic).",
142+
)
143+
suppression: Optional[Suppression] = Field(
144+
None, description="Configuration details for suppressing an alarm."
145+
)
146+
is_enabled: Optional[bool] = Field(
147+
None, description="Whether the alarm is enabled."
148+
)
149+
is_notifications_per_metric_dimension_enabled: Optional[bool] = Field(
150+
None,
151+
description="Whether the alarm sends a separate message for each metric stream.",
152+
)
153+
freeform_tags: Optional[Dict[str, str]] = Field(
154+
None, description="Simple key/value pair tags applied without predefined names."
155+
)
156+
defined_tags: Optional[Dict[str, Dict[str, Any]]] = Field(
157+
None, description="Defined tags for this resource, scoped to namespaces."
158+
)
159+
lifecycle_state: Optional[str] = Field(
160+
None, description="The current lifecycle state of the alarm."
161+
)
162+
overrides: Optional[List[AlarmOverride]] = Field(
163+
None,
164+
description="Overrides controlling alarm evaluations (query, severity, body, pending duration).",
165+
)
166+
rule_name: Optional[str] = Field(
167+
None,
168+
description="Identifier of the alarm’s base values when overrides are present; default 'BASE'.",
169+
)
170+
notification_version: Optional[str] = Field(
171+
None,
172+
description="Version of the alarm notification to be delivered (e.g., '1.X').",
173+
)
174+
notification_title: Optional[str] = Field(
175+
None,
176+
description="Customizable notification title used as subject/title in messages.",
177+
)
178+
evaluation_slack_duration: Optional[str] = Field(
179+
None,
180+
description="Slack period for metric ingestion before evaluating the alarm, ISO 8601 (e.g., 'PT3M').",
181+
)
182+
alarm_summary: Optional[str] = Field(
183+
None,
184+
description="Customizable alarm summary (message body) with optional dynamic variables.",
185+
)
186+
resource_group: Optional[str] = Field(
187+
None,
188+
description="Resource group to match for metrics used by this alarm.",
189+
)
190+
191+
192+
def map_alarm_summary(
193+
alarm: oci.monitoring.models.AlarmSummary,
194+
) -> AlarmSummary:
195+
"""
196+
Convert an oci.monitoring.models.AlarmSummary to
197+
oracle.oci_monitoring_mcp_server.alarms.models.AlarmSummary, including nested types.
198+
"""
199+
return AlarmSummary(
200+
id=getattr(alarm, "id", None),
201+
display_name=getattr(alarm, "display_name", None)
202+
or getattr(alarm, "displayName", None),
203+
compartment_id=getattr(alarm, "compartment_id", None)
204+
or getattr(alarm, "compartmentId", None),
205+
metric_compartment_id=getattr(alarm, "metric_compartment_id", None)
206+
or getattr(alarm, "metricCompartmentId", None),
207+
namespace=getattr(alarm, "namespace", None),
208+
query=getattr(alarm, "query", None),
209+
severity=getattr(alarm, "severity", None),
210+
destinations=getattr(alarm, "destinations", None),
211+
suppression=map_suppression(getattr(alarm, "suppression", None)),
212+
is_enabled=getattr(alarm, "is_enabled", None)
213+
or getattr(alarm, "isEnabled", None),
214+
is_notifications_per_metric_dimension_enabled=getattr(
215+
alarm, "is_notifications_per_metric_dimension_enabled", None
216+
)
217+
or getattr(alarm, "isNotificationsPerMetricDimensionEnabled", None),
218+
freeform_tags=getattr(alarm, "freeform_tags", None)
219+
or getattr(alarm, "freeformTags", None),
220+
defined_tags=getattr(alarm, "defined_tags", None)
221+
or getattr(alarm, "definedTags", None),
222+
lifecycle_state=getattr(alarm, "lifecycle_state", None)
223+
or getattr(alarm, "lifecycleState", None),
224+
overrides=map_alarm_overrides(getattr(alarm, "overrides", None)),
225+
rule_name=getattr(alarm, "rule_name", None) or getattr(alarm, "ruleName", None),
226+
notification_version=getattr(alarm, "notification_version", None)
227+
or getattr(alarm, "notificationVersion", None),
228+
notification_title=getattr(alarm, "notification_title", None)
229+
or getattr(alarm, "notificationTitle", None),
230+
evaluation_slack_duration=getattr(alarm, "evaluation_slack_duration", None)
231+
or getattr(alarm, "evaluationSlackDuration", None),
232+
alarm_summary=getattr(alarm, "alarm_summary", None)
233+
or getattr(alarm, "alarmSummary", None),
234+
resource_group=getattr(alarm, "resource_group", None)
235+
or getattr(alarm, "resourceGroup", None),
236+
)

src/oci-monitoring-mcp-server/oracle/oci_monitoring_mcp_server/alarms/tools.py

Lines changed: 17 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,16 @@
66

77
import os
88
from logging import Logger
9-
from typing import Annotated
9+
from typing import Annotated, List
1010

1111
import oci
1212
from fastmcp import FastMCP
13+
from oci import Response
1314
from oracle.oci_monitoring_mcp_server import __project__, __version__
15+
from oracle.oci_monitoring_mcp_server.alarms.models import (
16+
AlarmSummary,
17+
map_alarm_summary,
18+
)
1419

1520
logger = Logger(__name__, level="INFO")
1621

@@ -19,12 +24,14 @@
1924

2025
class MonitoringAlarmTools:
2126
def __init__(self):
22-
logger.info("Loaded metric metadata entries")
27+
logger.info("Loaded alarm class")
2328

2429
def register(self, mcp):
25-
"""Register all Metrics tools with the MCP server."""
30+
"""Register all alarm tools with the MCP server."""
2631
# Register list_alarms tool
27-
mcp.tool(name="list_alarms")(self.list_alarms)
32+
mcp.tool(
33+
name="list_alarms", description="Lists all alarms in a given compartment"
34+
)(self.list_alarms)
2835

2936
def get_monitoring_client(self):
3037
logger.info("entering get_monitoring_client")
@@ -49,20 +56,10 @@ def list_alarms(
4956
"The ID of the compartment containing the resources"
5057
"monitored by the metric that you are searching for.",
5158
],
52-
) -> list[dict]:
59+
) -> list[AlarmSummary]:
5360
monitoring_client = self.get_monitoring_client()
54-
response = monitoring_client.list_alarms(compartment_id=compartment_id)
55-
alarms = response.data
56-
result = []
57-
for alarm in alarms:
58-
result.append(
59-
{
60-
"id": alarm.id,
61-
"display_name": alarm.display_name,
62-
"severity": alarm.severity,
63-
"lifecycle_state": alarm.lifecycle_state,
64-
"namespace": alarm.namespace,
65-
"query": alarm.query,
66-
}
67-
)
68-
return result
61+
response: Response = monitoring_client.list_alarms(
62+
compartment_id=compartment_id
63+
)
64+
alarms: List[oci.monitoring.models.AlarmSummary] = response.data
65+
return [map_alarm_summary(alarm) for alarm in alarms]

0 commit comments

Comments
 (0)