Skip to content

Commit cd9e08c

Browse files
Merge branch 'main' into dependabot/pip/cloud-infrastructure/compute-including-hpc/ai-infra-gpu/ai-infrastructure/bionemo/alphafold2-oke/helm/nbconvert-7.17.0
2 parents 4559453 + a187a62 commit cd9e08c

121 files changed

Lines changed: 15198 additions & 65 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

ai/gen-ai-agents/mcp-oci-integration/files/consumption_utils.py

Lines changed: 500 additions & 14 deletions
Large diffs are not rendered by default.
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
"""
2+
File name: mcp_agenda.py
3+
Author: Luigi Saetta
4+
Date last modified: 2025-12-04
5+
Python Version: 3.11
6+
7+
Description:
8+
This module implements an MCP (Model Context Protocol) server for agenda and calendar management.
9+
It provides tools to list events overlapping with a given time interval, create new events with automatic conflict detection,
10+
and delete events by unique ID, using ISO date formats and structured JSON outputs for events and conflicts.
11+
12+
Usage:
13+
Import this module to use its tools or run it as a standalone MCP server.
14+
Example:
15+
from mcp_servers.mcp_agenda import create_event
16+
17+
event = create_event(title="Meeting", start_date="2025-12-05T10:00:00", end_date="2025-12-05T11:00:00")
18+
# Or run the server: python mcp_agenda.py
19+
20+
License:
21+
This code is released under the MIT License.
22+
23+
Notes:
24+
This is part of the MCP-OCI integration framework and uses in-memory storage for events (persistent options can be added).
25+
Tools return dictionaries with details like event IDs, conflicts, and messages for easy integration with MCP agents.
26+
27+
Warnings:
28+
This module is in development and may change in future versions. Ensure date inputs are in valid ISO formats to avoid errors,
29+
and note that events are created even with conflicts—handle them appropriately in applications.
30+
"""
31+
32+
from typing import List, Dict, Any, Optional
33+
from mcp_utils import create_server, run_server
34+
from agenda_utils import add_event, get_events, delete_event as delete_event_util
35+
from agenda_utils import init_random_events_for_current_week
36+
37+
mcp = create_server("Agenda MCP")
38+
39+
40+
@mcp.tool()
41+
def list_events(start_date: str, end_date: str) -> Dict[str, Any]:
42+
"""
43+
List all events in the agenda that overlap with the given interval.
44+
45+
Parameters
46+
----------
47+
start_date : str
48+
Start of the desired interval.
49+
Accepted formats:
50+
- 'YYYY-MM-DDTHH:MM:SS'
51+
- 'YYYY-MM-DD HH:MM'
52+
- 'YYYY-MM-DD'
53+
end_date : str
54+
End of the desired interval.
55+
Same accepted formats as start_date.
56+
57+
Returns
58+
-------
59+
dict
60+
JSON object with a single key "events":
61+
62+
{
63+
"events": [
64+
{
65+
"id": int,
66+
"title": str,
67+
"start": str, # ISO 8601
68+
"end": str, # ISO 8601
69+
"notes": str
70+
},
71+
...
72+
]
73+
}
74+
75+
Notes
76+
-----
77+
- Use the 'id' field from each event when you need to delete it.
78+
"""
79+
events = get_events(start_date, end_date)
80+
81+
return {"events": events}
82+
83+
84+
@mcp.tool()
85+
def create_event(
86+
title: str,
87+
start_date: str,
88+
end_date: str,
89+
notes: Optional[str] = None,
90+
) -> Dict[str, Any]:
91+
"""
92+
Create a new event in the agenda and report any scheduling conflicts.
93+
94+
Parameters
95+
----------
96+
title : str
97+
Short human-readable title of the event.
98+
start_date : str
99+
Start of the event.
100+
Accepted formats:
101+
- 'YYYY-MM-DDTHH:MM:SS'
102+
- 'YYYY-MM-DD HH:MM'
103+
- 'YYYY-MM-DD'
104+
end_date : str
105+
End of the event.
106+
Must be strictly after start_date.
107+
Same accepted formats as start_date.
108+
notes : str, optional
109+
Optional longer description or notes for the event.
110+
111+
Returns
112+
-------
113+
dict
114+
JSON object describing the created event and any conflicts:
115+
116+
{
117+
"id": int,
118+
"title": str,
119+
"start": str, # ISO 8601
120+
"end": str, # ISO 8601
121+
"notes": str | None,
122+
"has_conflict": bool,
123+
"conflicts": [
124+
{
125+
"id": int,
126+
"title": str,
127+
"start": str, # ISO 8601
128+
"end": str, # ISO 8601
129+
"notes": str | None
130+
},
131+
...
132+
]
133+
}
134+
135+
Notes
136+
-----
137+
- The event is always created, even if there are conflicts.
138+
- If dates are invalid or end_date <= start_date, a ValueError is raised.
139+
- The returned 'id' is the unique identifier to use for deletion.
140+
"""
141+
return add_event(title, start_date, end_date, notes=notes)
142+
143+
144+
@mcp.tool()
145+
def delete_event(event_id: int) -> Dict[str, Any]:
146+
"""
147+
Delete a single event by its unique ID.
148+
149+
Parameters
150+
----------
151+
event_id : int
152+
The unique identifier of the event, as returned by create_event or
153+
list_events (field 'id').
154+
155+
Returns
156+
-------
157+
dict
158+
JSON-safe result object:
159+
160+
{
161+
"deleted": bool,
162+
"event": {
163+
"id": int,
164+
"title": str,
165+
"start": str, # ISO 8601
166+
"end": str, # ISO 8601
167+
"notes": str | None
168+
} | None,
169+
"message": str
170+
}
171+
172+
Notes
173+
-----
174+
- If no event with the given ID exists, 'deleted' will be False and
175+
'event' will be null.
176+
"""
177+
return delete_event_util(event_id)
178+
179+
180+
if __name__ == "__main__":
181+
# initialise the agenda with some random events for the current week
182+
init_random_events_for_current_week()
183+
184+
run_server(mcp)
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
"""
2+
File name: mcp_base.py
3+
Author: Luigi Saetta
4+
Date last modified: 2025-12-04
5+
Python Version: 3.11
6+
7+
Description:
8+
This module provides the base class for MCP (Model Context Protocol) servers.
9+
It defines abstract methods and common functionality for server implementations,
10+
such as handling requests, authentication, and error management in a distributed system.
11+
12+
Usage:
13+
Import this module and subclass MCPBase to create custom server implementations.
14+
Example:
15+
from mcp_servers.mcp_base import MCPBase
16+
17+
class MyServer(MCPBase):
18+
def process_request(self, request_data):
19+
# Implement custom logic here
20+
pass
21+
22+
License:
23+
This code is released under the MIT License.
24+
25+
Notes:
26+
This is a foundational part of the MCP-OCI integration framework, designed for extensibility.
27+
Subclasses should implement all abstract methods to ensure compatibility with MCP tools and APIs.
28+
29+
Warnings:
30+
This module is in development and may change in future versions. Ensure subclasses handle
31+
potential breaking changes in abstract method signatures.
32+
"""
33+
34+
from typing import Callable, Optional
35+
from functools import wraps
36+
37+
from mcp_utils import create_server, run_server
38+
from utils import get_console_logger
39+
40+
41+
def expose_tool(name: Optional[str] = None, description: Optional[str] = None):
42+
"""
43+
Marker decorator for instance methods to export as MCP tools.
44+
45+
Args:
46+
name: Optional tool name; defaults to method name.
47+
description: Optional doc override shown in MCP discovery.
48+
"""
49+
50+
def _decorator(fn: Callable):
51+
setattr(fn, "_mcp_expose", True)
52+
if name:
53+
setattr(fn, "_mcp_tool_name", name)
54+
if description:
55+
setattr(fn, "_mcp_tool_desc", description)
56+
return fn
57+
58+
return _decorator
59+
60+
61+
class BaseMCPServer:
62+
"""
63+
Subclass and decorate instance methods with @expose_tool to export them.
64+
65+
Parameters
66+
----------
67+
server_name : str
68+
The MCP server name (surfaced in discovery).
69+
debug : bool
70+
Toggle extra logging.
71+
wrap_result : bool
72+
If True, wrap successful results as {"result": <value>}.
73+
on_call : Optional[Callable[[str, tuple, dict], None]]
74+
Optional hook invoked before each tool call with (tool_name, args, kwargs).
75+
on_error : Optional[Callable[[str, Exception], None]]
76+
Optional hook invoked when a tool raises.
77+
"""
78+
79+
def __init__(
80+
self,
81+
server_name: str,
82+
*,
83+
debug: bool = False,
84+
wrap_result: bool = False,
85+
on_call: Optional[Callable[[str, tuple, dict], None]] = None,
86+
on_error: Optional[Callable[[str, Exception], None]] = None,
87+
):
88+
self.server_name = server_name
89+
self.debug = debug
90+
self.wrap_result = wrap_result
91+
self.on_call = on_call
92+
self.on_error = on_error
93+
94+
self.logger = get_console_logger()
95+
# SECURITY/JWT is handled entirely by create_server; nothing here.
96+
self.mcp = create_server(server_name)
97+
98+
self._register_tools()
99+
100+
def _wrap_tool(self, method: Callable, tool_name: str, tool_desc: Optional[str]):
101+
"""
102+
Wrap the bound method with logging, optional hooks, result wrapping, and error handling.
103+
"""
104+
105+
@wraps(method)
106+
def _wrapped(*args, **kwargs):
107+
if self.debug:
108+
self.logger.info(
109+
"[%s] %s called | args=%s kwargs=%s",
110+
self.server_name,
111+
tool_name,
112+
args,
113+
kwargs,
114+
)
115+
if self.on_call:
116+
try:
117+
self.on_call(tool_name, args, kwargs)
118+
except Exception:
119+
# Never let hooks break the tool
120+
pass
121+
122+
try:
123+
out = method(*args, **kwargs)
124+
# Uniform, JSON-serializable success path
125+
return {"result": out} if self.wrap_result else out
126+
except Exception as e:
127+
if self.on_error:
128+
try:
129+
self.on_error(tool_name, e)
130+
except Exception:
131+
pass
132+
self.logger.exception(
133+
"[%s] %s error: %s", self.server_name, tool_name, e
134+
)
135+
# Always return a serializable error envelope
136+
return {"error": str(e)}
137+
138+
# Description for MCP discovery
139+
if tool_desc:
140+
_wrapped.__doc__ = tool_desc
141+
elif not getattr(_wrapped, "__doc__", None):
142+
_wrapped.__doc__ = f"MCP tool: {tool_name}"
143+
144+
# Programmatic registration (same effect as @mcp.tool)
145+
self.mcp.tool(_wrapped)
146+
return _wrapped
147+
148+
def _register_tools(self):
149+
"""
150+
Find and register all @expose_tool methods.
151+
"""
152+
for attr_name in dir(self):
153+
if attr_name.startswith("_"):
154+
continue
155+
method = getattr(self, attr_name)
156+
if not callable(method):
157+
continue
158+
if getattr(method, "_mcp_expose", False):
159+
tool_name = getattr(method, "_mcp_tool_name", attr_name)
160+
tool_desc = getattr(method, "_mcp_tool_desc", None)
161+
self._wrap_tool(method, tool_name, tool_desc)
162+
163+
def run(self):
164+
"""
165+
Delegate to shared run_server (reads host/port etc. from CLI).
166+
"""
167+
if self.debug:
168+
self.logger.info("Starting MCP server '%s'...", self.server_name)
169+
run_server(self.mcp)

0 commit comments

Comments
 (0)