Skip to content

Commit 8f47eeb

Browse files
committed
feat(monitoring): enhance server
1 parent c8ac65b commit 8f47eeb

File tree

9 files changed

+1230
-226
lines changed

9 files changed

+1230
-226
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+
)
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
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+
import os
8+
from logging import Logger
9+
from typing import Annotated, List
10+
11+
import oci
12+
from fastmcp import FastMCP
13+
from oci import Response
14+
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+
)
19+
20+
logger = Logger(__name__, level="INFO")
21+
22+
mcp = FastMCP(name=__project__)
23+
24+
25+
class MonitoringAlarmTools:
26+
def __init__(self):
27+
logger.info("Loaded alarm class")
28+
29+
def register(self, mcp):
30+
"""Register all alarm tools with the MCP server."""
31+
# Register list_alarms tool
32+
mcp.tool(
33+
name="list_alarms", description="Lists all alarms in a given compartment"
34+
)(self.list_alarms)
35+
36+
def get_monitoring_client(self):
37+
logger.info("entering get_monitoring_client")
38+
config = oci.config.from_file(
39+
profile_name=os.getenv("OCI_CONFIG_PROFILE", oci.config.DEFAULT_PROFILE)
40+
)
41+
user_agent_name = __project__.split("oracle.", 1)[1].split("-server", 1)[0]
42+
config["additional_user_agent"] = f"{user_agent_name}/{__version__}"
43+
44+
private_key = oci.signer.load_private_key_from_file(config["key_file"])
45+
token_file = config["security_token_file"]
46+
token = None
47+
with open(token_file, "r") as f:
48+
token = f.read()
49+
signer = oci.auth.signers.SecurityTokenSigner(token, private_key)
50+
return oci.monitoring.MonitoringClient(config, signer=signer)
51+
52+
def list_alarms(
53+
self,
54+
compartment_id: Annotated[
55+
str,
56+
"The ID of the compartment containing the resources"
57+
"monitored by the metric that you are searching for.",
58+
],
59+
) -> list[AlarmSummary] | str:
60+
monitoring_client = self.get_monitoring_client()
61+
response: Response | None = monitoring_client.list_alarms(
62+
compartment_id=compartment_id
63+
)
64+
if response is None:
65+
logger.error("Received None response from list_metrics")
66+
return "There was no response returned from the Monitoring API"
67+
68+
alarms: List[oci.monitoring.models.AlarmSummary] = response.data
69+
return [map_alarm_summary(alarm) for alarm in alarms]

0 commit comments

Comments
 (0)