msgraph-mcp-server 0.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- msgraph_mcp/__init__.py +0 -0
- msgraph_mcp/auth/__init__.py +0 -0
- msgraph_mcp/auth/cli.py +45 -0
- msgraph_mcp/auth/msal_app.py +47 -0
- msgraph_mcp/auth/token.py +32 -0
- msgraph_mcp/config.py +79 -0
- msgraph_mcp/graph/__init__.py +0 -0
- msgraph_mcp/graph/auth_provider.py +29 -0
- msgraph_mcp/graph/batch.py +206 -0
- msgraph_mcp/graph/client.py +37 -0
- msgraph_mcp/graph/errors.py +43 -0
- msgraph_mcp/graph/pagination.py +47 -0
- msgraph_mcp/graph/serialize.py +351 -0
- msgraph_mcp/graph/trimming.py +359 -0
- msgraph_mcp/server.py +61 -0
- msgraph_mcp/tools/__init__.py +35 -0
- msgraph_mcp/tools/_binary.py +48 -0
- msgraph_mcp/tools/calendar.py +585 -0
- msgraph_mcp/tools/mail_actions.py +83 -0
- msgraph_mcp/tools/mail_batch.py +220 -0
- msgraph_mcp/tools/mail_folders.py +263 -0
- msgraph_mcp/tools/mail_read.py +333 -0
- msgraph_mcp/tools/mail_rules.py +468 -0
- msgraph_mcp/tools/mail_write.py +403 -0
- msgraph_mcp/tools/teams_channels.py +223 -0
- msgraph_mcp/tools/teams_chats.py +140 -0
- msgraph_mcp/tools/teams_content.py +150 -0
- msgraph_mcp/tools/util.py +35 -0
- msgraph_mcp_server-0.3.0.dist-info/METADATA +243 -0
- msgraph_mcp_server-0.3.0.dist-info/RECORD +33 -0
- msgraph_mcp_server-0.3.0.dist-info/WHEEL +4 -0
- msgraph_mcp_server-0.3.0.dist-info/entry_points.txt +3 -0
- msgraph_mcp_server-0.3.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,585 @@
|
|
|
1
|
+
"""Calendar tools."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from datetime import timedelta
|
|
6
|
+
from typing import Literal
|
|
7
|
+
|
|
8
|
+
from kiota_abstractions.base_request_configuration import RequestConfiguration
|
|
9
|
+
from msgraph.generated.models.attendee import Attendee
|
|
10
|
+
from msgraph.generated.models.attendee_base import AttendeeBase
|
|
11
|
+
from msgraph.generated.models.attendee_type import AttendeeType
|
|
12
|
+
from msgraph.generated.models.body_type import BodyType
|
|
13
|
+
from msgraph.generated.models.date_time_time_zone import DateTimeTimeZone
|
|
14
|
+
from msgraph.generated.models.email_address import EmailAddress
|
|
15
|
+
from msgraph.generated.models.event import Event
|
|
16
|
+
from msgraph.generated.models.item_body import ItemBody
|
|
17
|
+
from msgraph.generated.models.location import Location
|
|
18
|
+
from msgraph.generated.models.time_constraint import TimeConstraint
|
|
19
|
+
from msgraph.generated.models.time_slot import TimeSlot
|
|
20
|
+
from msgraph.generated.users.item.calendars.item.calendar_view.calendar_view_request_builder import (
|
|
21
|
+
CalendarViewRequestBuilder,
|
|
22
|
+
)
|
|
23
|
+
from msgraph.generated.users.item.events.item.accept.accept_post_request_body import (
|
|
24
|
+
AcceptPostRequestBody,
|
|
25
|
+
)
|
|
26
|
+
from msgraph.generated.users.item.events.item.cancel.cancel_post_request_body import (
|
|
27
|
+
CancelPostRequestBody,
|
|
28
|
+
)
|
|
29
|
+
from msgraph.generated.users.item.events.item.decline.decline_post_request_body import (
|
|
30
|
+
DeclinePostRequestBody,
|
|
31
|
+
)
|
|
32
|
+
from msgraph.generated.users.item.events.item.tentatively_accept.tentatively_accept_post_request_body import (
|
|
33
|
+
TentativelyAcceptPostRequestBody,
|
|
34
|
+
)
|
|
35
|
+
from msgraph.generated.users.item.find_meeting_times.find_meeting_times_post_request_body import (
|
|
36
|
+
FindMeetingTimesPostRequestBody,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
from msgraph_mcp.auth.token import NotAuthenticatedError
|
|
40
|
+
from msgraph_mcp.graph.errors import GraphValidationError, map_kiota_error
|
|
41
|
+
from msgraph_mcp.graph.pagination import (
|
|
42
|
+
decode_page_token,
|
|
43
|
+
encode_next_link,
|
|
44
|
+
validate_limit,
|
|
45
|
+
)
|
|
46
|
+
from msgraph_mcp.graph.serialize import calendar_to_dict, event_to_dict
|
|
47
|
+
from msgraph_mcp.graph.trimming import trim_calendar, trim_event
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
_VALID_RESPONSES = {
|
|
51
|
+
"accept": "accepted",
|
|
52
|
+
"tentativelyAccept": "tentativelyAccepted",
|
|
53
|
+
"decline": "declined",
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
async def list_calendars(
|
|
58
|
+
*, graph, mailbox: str | None = None, include_raw: bool = False
|
|
59
|
+
) -> dict:
|
|
60
|
+
"""List the user's calendars."""
|
|
61
|
+
try:
|
|
62
|
+
collection = await graph.mailbox(mailbox).calendars.get()
|
|
63
|
+
except NotAuthenticatedError:
|
|
64
|
+
raise
|
|
65
|
+
except Exception as exc: # noqa: BLE001
|
|
66
|
+
raise map_kiota_error(exc) from exc
|
|
67
|
+
items = [
|
|
68
|
+
trim_calendar(calendar_to_dict(c), include_raw=include_raw)
|
|
69
|
+
for c in (collection.value or [])
|
|
70
|
+
]
|
|
71
|
+
return {"items": items, "next_page_token": None}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _calendar_view_query(*, start_datetime: str, end_datetime: str, limit: int):
|
|
75
|
+
qp = CalendarViewRequestBuilder.CalendarViewRequestBuilderGetQueryParameters(
|
|
76
|
+
start_date_time=start_datetime,
|
|
77
|
+
end_date_time=end_datetime,
|
|
78
|
+
top=limit,
|
|
79
|
+
orderby=["start/dateTime"],
|
|
80
|
+
)
|
|
81
|
+
return RequestConfiguration[
|
|
82
|
+
CalendarViewRequestBuilder.CalendarViewRequestBuilderGetQueryParameters
|
|
83
|
+
](query_parameters=qp)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
async def list_events(
|
|
87
|
+
*,
|
|
88
|
+
graph,
|
|
89
|
+
calendar_id: str,
|
|
90
|
+
start_datetime: str | None = None,
|
|
91
|
+
end_datetime: str | None = None,
|
|
92
|
+
mailbox: str | None = None,
|
|
93
|
+
limit: int = 25,
|
|
94
|
+
page_token: str | None = None,
|
|
95
|
+
include_raw: bool = False,
|
|
96
|
+
) -> dict:
|
|
97
|
+
"""List events in a calendar within a date range.
|
|
98
|
+
|
|
99
|
+
Uses Graph's calendarView, which expands recurring events into instances.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
calendar_id: Calendar ID. (Use list_calendars to discover.)
|
|
103
|
+
start_datetime: ISO 8601 datetime, e.g. "2026-05-19T00:00:00Z". Required.
|
|
104
|
+
end_datetime: ISO 8601 datetime. Required.
|
|
105
|
+
mailbox: Optional mailbox.
|
|
106
|
+
limit: 1-100. Default 25.
|
|
107
|
+
page_token: Continuation token.
|
|
108
|
+
include_raw: Include raw payloads.
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
{"items": [trimmed_event, ...], "next_page_token": str | None}
|
|
112
|
+
"""
|
|
113
|
+
if not (start_datetime and end_datetime):
|
|
114
|
+
raise GraphValidationError("start_datetime and end_datetime are required")
|
|
115
|
+
limit = validate_limit(limit)
|
|
116
|
+
cal_builder = graph.mailbox(mailbox).calendars.by_calendar_id(calendar_id)
|
|
117
|
+
try:
|
|
118
|
+
if page_token is not None:
|
|
119
|
+
url = decode_page_token(page_token)
|
|
120
|
+
collection = await cal_builder.calendar_view.with_url(url).get()
|
|
121
|
+
else:
|
|
122
|
+
collection = await cal_builder.calendar_view.get(
|
|
123
|
+
request_configuration=_calendar_view_query(
|
|
124
|
+
start_datetime=start_datetime, end_datetime=end_datetime, limit=limit
|
|
125
|
+
)
|
|
126
|
+
)
|
|
127
|
+
except NotAuthenticatedError:
|
|
128
|
+
raise
|
|
129
|
+
except Exception as exc: # noqa: BLE001
|
|
130
|
+
raise map_kiota_error(exc) from exc
|
|
131
|
+
|
|
132
|
+
items = [
|
|
133
|
+
trim_event(event_to_dict(e), include_body=False, include_raw=include_raw)
|
|
134
|
+
for e in (collection.value or [])
|
|
135
|
+
]
|
|
136
|
+
return {
|
|
137
|
+
"items": items,
|
|
138
|
+
"next_page_token": encode_next_link(getattr(collection, "odata_next_link", None)),
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
async def get_event(
|
|
143
|
+
*,
|
|
144
|
+
graph,
|
|
145
|
+
event_id: str,
|
|
146
|
+
mailbox: str | None = None,
|
|
147
|
+
include_body: bool = False,
|
|
148
|
+
include_raw: bool = False,
|
|
149
|
+
) -> dict:
|
|
150
|
+
"""Fetch a single event by id."""
|
|
151
|
+
try:
|
|
152
|
+
evt = await graph.mailbox(mailbox).events.by_event_id(event_id).get()
|
|
153
|
+
except NotAuthenticatedError:
|
|
154
|
+
raise
|
|
155
|
+
except Exception as exc: # noqa: BLE001
|
|
156
|
+
raise map_kiota_error(exc) from exc
|
|
157
|
+
return trim_event(event_to_dict(evt), include_body=include_body, include_raw=include_raw)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _dttz(dt: str, tz: str) -> DateTimeTimeZone:
|
|
161
|
+
obj = DateTimeTimeZone()
|
|
162
|
+
obj.date_time = dt
|
|
163
|
+
obj.time_zone = tz
|
|
164
|
+
return obj
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _build_event(
|
|
168
|
+
*,
|
|
169
|
+
subject: str,
|
|
170
|
+
start_datetime: str,
|
|
171
|
+
end_datetime: str,
|
|
172
|
+
time_zone: str,
|
|
173
|
+
body: str | None,
|
|
174
|
+
body_type: Literal["text", "html"],
|
|
175
|
+
location: str | None,
|
|
176
|
+
attendees: list[str] | None,
|
|
177
|
+
is_online_meeting: bool,
|
|
178
|
+
is_all_day: bool,
|
|
179
|
+
) -> Event:
|
|
180
|
+
e = Event()
|
|
181
|
+
e.subject = subject
|
|
182
|
+
e.start = _dttz(start_datetime, time_zone)
|
|
183
|
+
e.end = _dttz(end_datetime, time_zone)
|
|
184
|
+
if body is not None:
|
|
185
|
+
b = ItemBody()
|
|
186
|
+
b.content_type = BodyType.Text if body_type == "text" else BodyType.Html
|
|
187
|
+
b.content = body
|
|
188
|
+
e.body = b
|
|
189
|
+
if location is not None:
|
|
190
|
+
loc = Location()
|
|
191
|
+
loc.display_name = location
|
|
192
|
+
e.location = loc
|
|
193
|
+
if attendees:
|
|
194
|
+
atts: list[Attendee] = []
|
|
195
|
+
for addr in attendees:
|
|
196
|
+
a = Attendee()
|
|
197
|
+
ea = EmailAddress()
|
|
198
|
+
ea.address = addr
|
|
199
|
+
a.email_address = ea
|
|
200
|
+
a.type = AttendeeType.Required
|
|
201
|
+
atts.append(a)
|
|
202
|
+
e.attendees = atts
|
|
203
|
+
e.is_online_meeting = is_online_meeting
|
|
204
|
+
e.is_all_day = is_all_day
|
|
205
|
+
return e
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
async def create_event(
|
|
209
|
+
*,
|
|
210
|
+
graph,
|
|
211
|
+
subject: str,
|
|
212
|
+
start_datetime: str,
|
|
213
|
+
end_datetime: str,
|
|
214
|
+
time_zone: str = "UTC",
|
|
215
|
+
body: str | None = None,
|
|
216
|
+
body_type: Literal["text", "html"] = "text",
|
|
217
|
+
location: str | None = None,
|
|
218
|
+
attendees: list[str] | None = None,
|
|
219
|
+
is_online_meeting: bool = False,
|
|
220
|
+
is_all_day: bool = False,
|
|
221
|
+
calendar_id: str | None = None,
|
|
222
|
+
mailbox: str | None = None,
|
|
223
|
+
include_raw: bool = False,
|
|
224
|
+
) -> dict:
|
|
225
|
+
"""Create a calendar event.
|
|
226
|
+
|
|
227
|
+
Args:
|
|
228
|
+
subject: Event subject.
|
|
229
|
+
start_datetime: ISO 8601 datetime (no offset; pair with time_zone).
|
|
230
|
+
end_datetime: ISO 8601 datetime.
|
|
231
|
+
time_zone: IANA tz id (e.g. "America/Los_Angeles") or "UTC". Default "UTC".
|
|
232
|
+
body, body_type: Optional body and "text"|"html".
|
|
233
|
+
location: Optional location string.
|
|
234
|
+
attendees: Optional list of attendee email addresses (treated as required).
|
|
235
|
+
is_online_meeting: When True, Outlook adds a Teams meeting link.
|
|
236
|
+
is_all_day: All-day event flag.
|
|
237
|
+
calendar_id: Optional calendar id; default is the user's primary calendar.
|
|
238
|
+
mailbox: Optional mailbox.
|
|
239
|
+
|
|
240
|
+
Returns:
|
|
241
|
+
Trimmed created event.
|
|
242
|
+
"""
|
|
243
|
+
evt = _build_event(
|
|
244
|
+
subject=subject, start_datetime=start_datetime, end_datetime=end_datetime,
|
|
245
|
+
time_zone=time_zone, body=body, body_type=body_type, location=location,
|
|
246
|
+
attendees=attendees, is_online_meeting=is_online_meeting, is_all_day=is_all_day,
|
|
247
|
+
)
|
|
248
|
+
try:
|
|
249
|
+
target = graph.mailbox(mailbox)
|
|
250
|
+
if calendar_id is not None:
|
|
251
|
+
target = target.calendars.by_calendar_id(calendar_id)
|
|
252
|
+
created = await target.events.post(evt)
|
|
253
|
+
except NotAuthenticatedError:
|
|
254
|
+
raise
|
|
255
|
+
except Exception as exc: # noqa: BLE001
|
|
256
|
+
raise map_kiota_error(exc) from exc
|
|
257
|
+
return trim_event(event_to_dict(created), include_body=False, include_raw=include_raw)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
async def update_event(
|
|
261
|
+
*,
|
|
262
|
+
graph,
|
|
263
|
+
event_id: str,
|
|
264
|
+
subject: str | None = None,
|
|
265
|
+
start_datetime: str | None = None,
|
|
266
|
+
end_datetime: str | None = None,
|
|
267
|
+
time_zone: str | None = None,
|
|
268
|
+
body: str | None = None,
|
|
269
|
+
body_type: Literal["text", "html"] = "text",
|
|
270
|
+
location: str | None = None,
|
|
271
|
+
is_online_meeting: bool | None = None,
|
|
272
|
+
is_all_day: bool | None = None,
|
|
273
|
+
mailbox: str | None = None,
|
|
274
|
+
include_raw: bool = False,
|
|
275
|
+
) -> dict:
|
|
276
|
+
"""Patch fields of an existing event. Pass None to leave a field unchanged.
|
|
277
|
+
|
|
278
|
+
If you change start_datetime or end_datetime, you must also pass time_zone.
|
|
279
|
+
"""
|
|
280
|
+
if (start_datetime or end_datetime) and not time_zone:
|
|
281
|
+
raise GraphValidationError("time_zone is required when updating start or end")
|
|
282
|
+
patch = Event()
|
|
283
|
+
if subject is not None:
|
|
284
|
+
patch.subject = subject
|
|
285
|
+
if start_datetime is not None:
|
|
286
|
+
patch.start = _dttz(start_datetime, time_zone or "UTC")
|
|
287
|
+
if end_datetime is not None:
|
|
288
|
+
patch.end = _dttz(end_datetime, time_zone or "UTC")
|
|
289
|
+
if body is not None:
|
|
290
|
+
b = ItemBody()
|
|
291
|
+
b.content_type = BodyType.Text if body_type == "text" else BodyType.Html
|
|
292
|
+
b.content = body
|
|
293
|
+
patch.body = b
|
|
294
|
+
if location is not None:
|
|
295
|
+
loc = Location()
|
|
296
|
+
loc.display_name = location
|
|
297
|
+
patch.location = loc
|
|
298
|
+
if is_online_meeting is not None:
|
|
299
|
+
patch.is_online_meeting = is_online_meeting
|
|
300
|
+
if is_all_day is not None:
|
|
301
|
+
patch.is_all_day = is_all_day
|
|
302
|
+
try:
|
|
303
|
+
updated = await graph.mailbox(mailbox).events.by_event_id(event_id).patch(patch)
|
|
304
|
+
except NotAuthenticatedError:
|
|
305
|
+
raise
|
|
306
|
+
except Exception as exc: # noqa: BLE001
|
|
307
|
+
raise map_kiota_error(exc) from exc
|
|
308
|
+
return trim_event(event_to_dict(updated), include_body=False, include_raw=include_raw)
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
async def delete_event(*, graph, event_id: str, mailbox: str | None = None) -> dict:
|
|
312
|
+
"""Delete an event without sending a cancellation to attendees."""
|
|
313
|
+
try:
|
|
314
|
+
await graph.mailbox(mailbox).events.by_event_id(event_id).delete()
|
|
315
|
+
except NotAuthenticatedError:
|
|
316
|
+
raise
|
|
317
|
+
except Exception as exc: # noqa: BLE001
|
|
318
|
+
raise map_kiota_error(exc) from exc
|
|
319
|
+
return {"status": "deleted"}
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
async def cancel_event(
|
|
323
|
+
*, graph, event_id: str, comment: str | None = None, mailbox: str | None = None
|
|
324
|
+
) -> dict:
|
|
325
|
+
"""Cancel an event and send a cancellation notice to attendees."""
|
|
326
|
+
body = CancelPostRequestBody()
|
|
327
|
+
if comment is not None:
|
|
328
|
+
body.comment = comment
|
|
329
|
+
try:
|
|
330
|
+
await graph.mailbox(mailbox).events.by_event_id(event_id).cancel.post(body)
|
|
331
|
+
except NotAuthenticatedError:
|
|
332
|
+
raise
|
|
333
|
+
except Exception as exc: # noqa: BLE001
|
|
334
|
+
raise map_kiota_error(exc) from exc
|
|
335
|
+
return {"status": "cancelled"}
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
async def respond_to_event(
|
|
339
|
+
*,
|
|
340
|
+
graph,
|
|
341
|
+
event_id: str,
|
|
342
|
+
response: Literal["accept", "tentativelyAccept", "decline"],
|
|
343
|
+
comment: str | None = None,
|
|
344
|
+
send_response: bool = True,
|
|
345
|
+
mailbox: str | None = None,
|
|
346
|
+
) -> dict:
|
|
347
|
+
"""Respond to a meeting invite.
|
|
348
|
+
|
|
349
|
+
Args:
|
|
350
|
+
response: One of "accept", "tentativelyAccept", "decline".
|
|
351
|
+
comment: Optional comment included with the response.
|
|
352
|
+
send_response: When False, your response status is recorded without
|
|
353
|
+
emailing the organizer.
|
|
354
|
+
"""
|
|
355
|
+
if response not in _VALID_RESPONSES:
|
|
356
|
+
raise GraphValidationError(
|
|
357
|
+
f"response must be one of {sorted(_VALID_RESPONSES)}"
|
|
358
|
+
)
|
|
359
|
+
evt_builder = graph.mailbox(mailbox).events.by_event_id(event_id)
|
|
360
|
+
try:
|
|
361
|
+
if response == "accept":
|
|
362
|
+
accept_body = AcceptPostRequestBody()
|
|
363
|
+
accept_body.comment = comment
|
|
364
|
+
accept_body.send_response = send_response
|
|
365
|
+
await evt_builder.accept.post(accept_body)
|
|
366
|
+
elif response == "tentativelyAccept":
|
|
367
|
+
tentative_body = TentativelyAcceptPostRequestBody()
|
|
368
|
+
tentative_body.comment = comment
|
|
369
|
+
tentative_body.send_response = send_response
|
|
370
|
+
await evt_builder.tentatively_accept.post(tentative_body)
|
|
371
|
+
else: # decline
|
|
372
|
+
decline_body = DeclinePostRequestBody()
|
|
373
|
+
decline_body.comment = comment
|
|
374
|
+
decline_body.send_response = send_response
|
|
375
|
+
await evt_builder.decline.post(decline_body)
|
|
376
|
+
except NotAuthenticatedError:
|
|
377
|
+
raise
|
|
378
|
+
except Exception as exc: # noqa: BLE001
|
|
379
|
+
raise map_kiota_error(exc) from exc
|
|
380
|
+
return {"status": _VALID_RESPONSES[response]}
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
def _duration_td(minutes: int) -> timedelta:
|
|
384
|
+
"""Return a timedelta for the given whole minutes.
|
|
385
|
+
|
|
386
|
+
The kiota serializer renders this as ISO 8601 (e.g. PT30M) on the wire.
|
|
387
|
+
"""
|
|
388
|
+
if minutes <= 0:
|
|
389
|
+
raise GraphValidationError("duration_minutes must be a positive integer")
|
|
390
|
+
return timedelta(minutes=int(minutes))
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
async def find_meeting_times(
|
|
394
|
+
*,
|
|
395
|
+
graph,
|
|
396
|
+
attendees: list[str],
|
|
397
|
+
duration_minutes: int,
|
|
398
|
+
start_window: str,
|
|
399
|
+
end_window: str,
|
|
400
|
+
mailbox: str | None = None,
|
|
401
|
+
max_candidates: int = 20,
|
|
402
|
+
include_raw: bool = False,
|
|
403
|
+
) -> dict:
|
|
404
|
+
"""Suggest meeting times that work for the given attendees.
|
|
405
|
+
|
|
406
|
+
Args:
|
|
407
|
+
attendees: List of attendee email addresses.
|
|
408
|
+
duration_minutes: Meeting duration in whole minutes (e.g. 30).
|
|
409
|
+
start_window: ISO 8601 start of the candidate window.
|
|
410
|
+
end_window: ISO 8601 end of the candidate window.
|
|
411
|
+
max_candidates: Cap on returned suggestions (default 20).
|
|
412
|
+
|
|
413
|
+
Returns:
|
|
414
|
+
{
|
|
415
|
+
"suggestions": [
|
|
416
|
+
{"start": {date_time, time_zone}, "end": {...}, "confidence": float, "order_hint": int},
|
|
417
|
+
...
|
|
418
|
+
],
|
|
419
|
+
"empty_reason": str | None,
|
|
420
|
+
}
|
|
421
|
+
"""
|
|
422
|
+
if not attendees:
|
|
423
|
+
raise GraphValidationError("`attendees` must be a non-empty list")
|
|
424
|
+
body = FindMeetingTimesPostRequestBody()
|
|
425
|
+
body.meeting_duration = _duration_td(duration_minutes)
|
|
426
|
+
body.max_candidates = max_candidates
|
|
427
|
+
body.attendees = []
|
|
428
|
+
for addr in attendees:
|
|
429
|
+
a = AttendeeBase()
|
|
430
|
+
ea = EmailAddress()
|
|
431
|
+
ea.address = addr
|
|
432
|
+
a.email_address = ea
|
|
433
|
+
body.attendees.append(a)
|
|
434
|
+
tc = TimeConstraint()
|
|
435
|
+
slot = TimeSlot()
|
|
436
|
+
slot.start = _dttz(start_window, "UTC")
|
|
437
|
+
slot.end = _dttz(end_window, "UTC")
|
|
438
|
+
tc.time_slots = [slot]
|
|
439
|
+
body.time_constraint = tc
|
|
440
|
+
try:
|
|
441
|
+
result = await graph.mailbox(mailbox).find_meeting_times.post(body)
|
|
442
|
+
except NotAuthenticatedError:
|
|
443
|
+
raise
|
|
444
|
+
except Exception as exc: # noqa: BLE001
|
|
445
|
+
raise map_kiota_error(exc) from exc
|
|
446
|
+
|
|
447
|
+
suggestions_raw = getattr(result, "meeting_time_suggestions", None) or []
|
|
448
|
+
suggestions = []
|
|
449
|
+
for s in suggestions_raw:
|
|
450
|
+
ts = getattr(s, "meeting_time_slot", None)
|
|
451
|
+
suggestions.append(
|
|
452
|
+
{
|
|
453
|
+
"start": {
|
|
454
|
+
"date_time": getattr(getattr(ts, "start", None), "date_time", None),
|
|
455
|
+
"time_zone": getattr(getattr(ts, "start", None), "time_zone", None),
|
|
456
|
+
} if ts else None,
|
|
457
|
+
"end": {
|
|
458
|
+
"date_time": getattr(getattr(ts, "end", None), "date_time", None),
|
|
459
|
+
"time_zone": getattr(getattr(ts, "end", None), "time_zone", None),
|
|
460
|
+
} if ts else None,
|
|
461
|
+
"confidence": getattr(s, "confidence", None),
|
|
462
|
+
"order_hint": getattr(s, "order_hint", None),
|
|
463
|
+
}
|
|
464
|
+
)
|
|
465
|
+
out: dict = {
|
|
466
|
+
"suggestions": suggestions,
|
|
467
|
+
"empty_reason": getattr(result, "empty_suggestions_reason", None),
|
|
468
|
+
}
|
|
469
|
+
if include_raw:
|
|
470
|
+
out["raw"] = getattr(result, "additional_data", {}) or {}
|
|
471
|
+
return out
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
def register(mcp, *, graph) -> None:
|
|
475
|
+
@mcp.tool(name="list_calendars", description=list_calendars.__doc__ or "")
|
|
476
|
+
async def _list_calendars(mailbox: str | None = None, include_raw: bool = False):
|
|
477
|
+
return await list_calendars(graph=graph, mailbox=mailbox, include_raw=include_raw)
|
|
478
|
+
|
|
479
|
+
@mcp.tool(name="list_events", description=list_events.__doc__ or "")
|
|
480
|
+
async def _list_events(
|
|
481
|
+
calendar_id: str,
|
|
482
|
+
start_datetime: str,
|
|
483
|
+
end_datetime: str,
|
|
484
|
+
mailbox: str | None = None,
|
|
485
|
+
limit: int = 25,
|
|
486
|
+
page_token: str | None = None,
|
|
487
|
+
include_raw: bool = False,
|
|
488
|
+
):
|
|
489
|
+
return await list_events(
|
|
490
|
+
graph=graph, calendar_id=calendar_id,
|
|
491
|
+
start_datetime=start_datetime, end_datetime=end_datetime,
|
|
492
|
+
mailbox=mailbox, limit=limit, page_token=page_token, include_raw=include_raw,
|
|
493
|
+
)
|
|
494
|
+
|
|
495
|
+
@mcp.tool(name="get_event", description=get_event.__doc__ or "")
|
|
496
|
+
async def _get_event(
|
|
497
|
+
event_id: str,
|
|
498
|
+
mailbox: str | None = None,
|
|
499
|
+
include_body: bool = False,
|
|
500
|
+
include_raw: bool = False,
|
|
501
|
+
):
|
|
502
|
+
return await get_event(
|
|
503
|
+
graph=graph, event_id=event_id, mailbox=mailbox,
|
|
504
|
+
include_body=include_body, include_raw=include_raw,
|
|
505
|
+
)
|
|
506
|
+
|
|
507
|
+
@mcp.tool(name="create_event", description=create_event.__doc__ or "")
|
|
508
|
+
async def _create_event(
|
|
509
|
+
subject: str, start_datetime: str, end_datetime: str,
|
|
510
|
+
time_zone: str = "UTC",
|
|
511
|
+
body: str | None = None,
|
|
512
|
+
body_type: Literal["text", "html"] = "text",
|
|
513
|
+
location: str | None = None, attendees: list[str] | None = None,
|
|
514
|
+
is_online_meeting: bool = False, is_all_day: bool = False,
|
|
515
|
+
calendar_id: str | None = None, mailbox: str | None = None,
|
|
516
|
+
include_raw: bool = False,
|
|
517
|
+
):
|
|
518
|
+
return await create_event(
|
|
519
|
+
graph=graph, subject=subject,
|
|
520
|
+
start_datetime=start_datetime, end_datetime=end_datetime,
|
|
521
|
+
time_zone=time_zone, body=body, body_type=body_type, location=location,
|
|
522
|
+
attendees=attendees, is_online_meeting=is_online_meeting, is_all_day=is_all_day,
|
|
523
|
+
calendar_id=calendar_id, mailbox=mailbox, include_raw=include_raw,
|
|
524
|
+
)
|
|
525
|
+
|
|
526
|
+
@mcp.tool(name="update_event", description=update_event.__doc__ or "")
|
|
527
|
+
async def _update_event(
|
|
528
|
+
event_id: str,
|
|
529
|
+
subject: str | None = None,
|
|
530
|
+
start_datetime: str | None = None,
|
|
531
|
+
end_datetime: str | None = None,
|
|
532
|
+
time_zone: str | None = None,
|
|
533
|
+
body: str | None = None,
|
|
534
|
+
body_type: Literal["text", "html"] = "text",
|
|
535
|
+
location: str | None = None,
|
|
536
|
+
is_online_meeting: bool | None = None,
|
|
537
|
+
is_all_day: bool | None = None,
|
|
538
|
+
mailbox: str | None = None,
|
|
539
|
+
include_raw: bool = False,
|
|
540
|
+
):
|
|
541
|
+
return await update_event(
|
|
542
|
+
graph=graph, event_id=event_id, subject=subject,
|
|
543
|
+
start_datetime=start_datetime, end_datetime=end_datetime, time_zone=time_zone,
|
|
544
|
+
body=body, body_type=body_type, location=location,
|
|
545
|
+
is_online_meeting=is_online_meeting, is_all_day=is_all_day,
|
|
546
|
+
mailbox=mailbox, include_raw=include_raw,
|
|
547
|
+
)
|
|
548
|
+
|
|
549
|
+
@mcp.tool(name="delete_event", description=delete_event.__doc__ or "")
|
|
550
|
+
async def _delete_event(event_id: str, mailbox: str | None = None):
|
|
551
|
+
return await delete_event(graph=graph, event_id=event_id, mailbox=mailbox)
|
|
552
|
+
|
|
553
|
+
@mcp.tool(name="cancel_event", description=cancel_event.__doc__ or "")
|
|
554
|
+
async def _cancel_event(
|
|
555
|
+
event_id: str, comment: str | None = None, mailbox: str | None = None
|
|
556
|
+
):
|
|
557
|
+
return await cancel_event(
|
|
558
|
+
graph=graph, event_id=event_id, comment=comment, mailbox=mailbox
|
|
559
|
+
)
|
|
560
|
+
|
|
561
|
+
@mcp.tool(name="respond_to_event", description=respond_to_event.__doc__ or "")
|
|
562
|
+
async def _respond(
|
|
563
|
+
event_id: str,
|
|
564
|
+
response: Literal["accept", "tentativelyAccept", "decline"],
|
|
565
|
+
comment: str | None = None,
|
|
566
|
+
send_response: bool = True,
|
|
567
|
+
mailbox: str | None = None,
|
|
568
|
+
):
|
|
569
|
+
return await respond_to_event(
|
|
570
|
+
graph=graph, event_id=event_id, response=response, comment=comment,
|
|
571
|
+
send_response=send_response, mailbox=mailbox,
|
|
572
|
+
)
|
|
573
|
+
|
|
574
|
+
@mcp.tool(name="find_meeting_times", description=find_meeting_times.__doc__ or "")
|
|
575
|
+
async def _find(
|
|
576
|
+
attendees: list[str], duration_minutes: int,
|
|
577
|
+
start_window: str, end_window: str,
|
|
578
|
+
mailbox: str | None = None, max_candidates: int = 20,
|
|
579
|
+
include_raw: bool = False,
|
|
580
|
+
):
|
|
581
|
+
return await find_meeting_times(
|
|
582
|
+
graph=graph, attendees=attendees, duration_minutes=duration_minutes,
|
|
583
|
+
start_window=start_window, end_window=end_window,
|
|
584
|
+
mailbox=mailbox, max_candidates=max_candidates, include_raw=include_raw,
|
|
585
|
+
)
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""Mail workflow shortcuts: archive, mark read/unread, flag/unflag.
|
|
2
|
+
|
|
3
|
+
These are thin wrappers around move_message / update_message but with
|
|
4
|
+
distinct tool names so the agent picks the right one for natural-language
|
|
5
|
+
intents like 'archive this' or 'mark as read'.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from msgraph_mcp.tools.mail_folders import move_message
|
|
11
|
+
from msgraph_mcp.tools.mail_write import update_message
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
async def archive_message(
|
|
15
|
+
*, graph, message_id: str, mailbox: str | None = None, include_raw: bool = False
|
|
16
|
+
) -> dict:
|
|
17
|
+
"""Archive a message (move it to the Archive folder)."""
|
|
18
|
+
return await move_message(
|
|
19
|
+
graph=graph, message_id=message_id, destination="archive",
|
|
20
|
+
mailbox=mailbox, include_raw=include_raw,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
async def mark_read(
|
|
25
|
+
*, graph, message_id: str, mailbox: str | None = None, include_raw: bool = False
|
|
26
|
+
) -> dict:
|
|
27
|
+
"""Mark a message as read."""
|
|
28
|
+
return await update_message(
|
|
29
|
+
graph=graph, message_id=message_id, is_read=True,
|
|
30
|
+
mailbox=mailbox, include_raw=include_raw,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
async def mark_unread(
|
|
35
|
+
*, graph, message_id: str, mailbox: str | None = None, include_raw: bool = False
|
|
36
|
+
) -> dict:
|
|
37
|
+
"""Mark a message as unread."""
|
|
38
|
+
return await update_message(
|
|
39
|
+
graph=graph, message_id=message_id, is_read=False,
|
|
40
|
+
mailbox=mailbox, include_raw=include_raw,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
async def flag_message(
|
|
45
|
+
*, graph, message_id: str, mailbox: str | None = None, include_raw: bool = False
|
|
46
|
+
) -> dict:
|
|
47
|
+
"""Set the follow-up flag on a message."""
|
|
48
|
+
return await update_message(
|
|
49
|
+
graph=graph, message_id=message_id, flag="flagged",
|
|
50
|
+
mailbox=mailbox, include_raw=include_raw,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
async def unflag_message(
|
|
55
|
+
*, graph, message_id: str, mailbox: str | None = None, include_raw: bool = False
|
|
56
|
+
) -> dict:
|
|
57
|
+
"""Clear the follow-up flag on a message."""
|
|
58
|
+
return await update_message(
|
|
59
|
+
graph=graph, message_id=message_id, flag="notFlagged",
|
|
60
|
+
mailbox=mailbox, include_raw=include_raw,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def register(mcp, *, graph) -> None:
|
|
65
|
+
@mcp.tool(name="archive_message", description=archive_message.__doc__ or "")
|
|
66
|
+
async def _archive(message_id: str, mailbox: str | None = None, include_raw: bool = False):
|
|
67
|
+
return await archive_message(graph=graph, message_id=message_id, mailbox=mailbox, include_raw=include_raw)
|
|
68
|
+
|
|
69
|
+
@mcp.tool(name="mark_read", description=mark_read.__doc__ or "")
|
|
70
|
+
async def _mark_read(message_id: str, mailbox: str | None = None, include_raw: bool = False):
|
|
71
|
+
return await mark_read(graph=graph, message_id=message_id, mailbox=mailbox, include_raw=include_raw)
|
|
72
|
+
|
|
73
|
+
@mcp.tool(name="mark_unread", description=mark_unread.__doc__ or "")
|
|
74
|
+
async def _mark_unread(message_id: str, mailbox: str | None = None, include_raw: bool = False):
|
|
75
|
+
return await mark_unread(graph=graph, message_id=message_id, mailbox=mailbox, include_raw=include_raw)
|
|
76
|
+
|
|
77
|
+
@mcp.tool(name="flag_message", description=flag_message.__doc__ or "")
|
|
78
|
+
async def _flag(message_id: str, mailbox: str | None = None, include_raw: bool = False):
|
|
79
|
+
return await flag_message(graph=graph, message_id=message_id, mailbox=mailbox, include_raw=include_raw)
|
|
80
|
+
|
|
81
|
+
@mcp.tool(name="unflag_message", description=unflag_message.__doc__ or "")
|
|
82
|
+
async def _unflag(message_id: str, mailbox: str | None = None, include_raw: bool = False):
|
|
83
|
+
return await unflag_message(graph=graph, message_id=message_id, mailbox=mailbox, include_raw=include_raw)
|