msgraph-mcp-server 0.3.2__tar.gz → 0.5.0__tar.gz
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_server-0.3.2 → msgraph_mcp_server-0.5.0}/PKG-INFO +1 -1
- {msgraph_mcp_server-0.3.2 → msgraph_mcp_server-0.5.0}/pyproject.toml +1 -1
- {msgraph_mcp_server-0.3.2 → msgraph_mcp_server-0.5.0}/server.json +2 -2
- {msgraph_mcp_server-0.3.2 → msgraph_mcp_server-0.5.0}/src/msgraph_mcp/graph/serialize.py +4 -0
- {msgraph_mcp_server-0.3.2 → msgraph_mcp_server-0.5.0}/src/msgraph_mcp/graph/trimming.py +2 -0
- {msgraph_mcp_server-0.3.2 → msgraph_mcp_server-0.5.0}/src/msgraph_mcp/tools/calendar.py +90 -0
- {msgraph_mcp_server-0.3.2 → msgraph_mcp_server-0.5.0}/tests/fixtures/event_minimal.json +2 -0
- {msgraph_mcp_server-0.3.2 → msgraph_mcp_server-0.5.0}/tests/test_graph_serialize.py +12 -0
- {msgraph_mcp_server-0.3.2 → msgraph_mcp_server-0.5.0}/tests/test_graph_trimming.py +7 -0
- {msgraph_mcp_server-0.3.2 → msgraph_mcp_server-0.5.0}/tests/test_tools_calendar.py +194 -0
- {msgraph_mcp_server-0.3.2 → msgraph_mcp_server-0.5.0}/uv.lock +1 -1
- {msgraph_mcp_server-0.3.2 → msgraph_mcp_server-0.5.0}/.env.example +0 -0
- {msgraph_mcp_server-0.3.2 → msgraph_mcp_server-0.5.0}/.github/dependabot.yml +0 -0
- {msgraph_mcp_server-0.3.2 → msgraph_mcp_server-0.5.0}/.github/workflows/ci.yml +0 -0
- {msgraph_mcp_server-0.3.2 → msgraph_mcp_server-0.5.0}/.github/workflows/release.yml +0 -0
- {msgraph_mcp_server-0.3.2 → msgraph_mcp_server-0.5.0}/.gitignore +0 -0
- {msgraph_mcp_server-0.3.2 → msgraph_mcp_server-0.5.0}/CONTRIBUTING.md +0 -0
- {msgraph_mcp_server-0.3.2 → msgraph_mcp_server-0.5.0}/LICENSE +0 -0
- {msgraph_mcp_server-0.3.2 → msgraph_mcp_server-0.5.0}/README.md +0 -0
- {msgraph_mcp_server-0.3.2 → msgraph_mcp_server-0.5.0}/src/msgraph_mcp/__init__.py +0 -0
- {msgraph_mcp_server-0.3.2 → msgraph_mcp_server-0.5.0}/src/msgraph_mcp/auth/__init__.py +0 -0
- {msgraph_mcp_server-0.3.2 → msgraph_mcp_server-0.5.0}/src/msgraph_mcp/auth/cli.py +0 -0
- {msgraph_mcp_server-0.3.2 → msgraph_mcp_server-0.5.0}/src/msgraph_mcp/auth/msal_app.py +0 -0
- {msgraph_mcp_server-0.3.2 → msgraph_mcp_server-0.5.0}/src/msgraph_mcp/auth/token.py +0 -0
- {msgraph_mcp_server-0.3.2 → msgraph_mcp_server-0.5.0}/src/msgraph_mcp/config.py +0 -0
- {msgraph_mcp_server-0.3.2 → msgraph_mcp_server-0.5.0}/src/msgraph_mcp/graph/__init__.py +0 -0
- {msgraph_mcp_server-0.3.2 → msgraph_mcp_server-0.5.0}/src/msgraph_mcp/graph/auth_provider.py +0 -0
- {msgraph_mcp_server-0.3.2 → msgraph_mcp_server-0.5.0}/src/msgraph_mcp/graph/batch.py +0 -0
- {msgraph_mcp_server-0.3.2 → msgraph_mcp_server-0.5.0}/src/msgraph_mcp/graph/client.py +0 -0
- {msgraph_mcp_server-0.3.2 → msgraph_mcp_server-0.5.0}/src/msgraph_mcp/graph/errors.py +0 -0
- {msgraph_mcp_server-0.3.2 → msgraph_mcp_server-0.5.0}/src/msgraph_mcp/graph/pagination.py +0 -0
- {msgraph_mcp_server-0.3.2 → msgraph_mcp_server-0.5.0}/src/msgraph_mcp/server.py +0 -0
- {msgraph_mcp_server-0.3.2 → msgraph_mcp_server-0.5.0}/src/msgraph_mcp/tools/__init__.py +0 -0
- {msgraph_mcp_server-0.3.2 → msgraph_mcp_server-0.5.0}/src/msgraph_mcp/tools/_binary.py +0 -0
- {msgraph_mcp_server-0.3.2 → msgraph_mcp_server-0.5.0}/src/msgraph_mcp/tools/mail_actions.py +0 -0
- {msgraph_mcp_server-0.3.2 → msgraph_mcp_server-0.5.0}/src/msgraph_mcp/tools/mail_batch.py +0 -0
- {msgraph_mcp_server-0.3.2 → msgraph_mcp_server-0.5.0}/src/msgraph_mcp/tools/mail_folders.py +0 -0
- {msgraph_mcp_server-0.3.2 → msgraph_mcp_server-0.5.0}/src/msgraph_mcp/tools/mail_read.py +0 -0
- {msgraph_mcp_server-0.3.2 → msgraph_mcp_server-0.5.0}/src/msgraph_mcp/tools/mail_rules.py +0 -0
- {msgraph_mcp_server-0.3.2 → msgraph_mcp_server-0.5.0}/src/msgraph_mcp/tools/mail_write.py +0 -0
- {msgraph_mcp_server-0.3.2 → msgraph_mcp_server-0.5.0}/src/msgraph_mcp/tools/teams_channels.py +0 -0
- {msgraph_mcp_server-0.3.2 → msgraph_mcp_server-0.5.0}/src/msgraph_mcp/tools/teams_chats.py +0 -0
- {msgraph_mcp_server-0.3.2 → msgraph_mcp_server-0.5.0}/src/msgraph_mcp/tools/teams_content.py +0 -0
- {msgraph_mcp_server-0.3.2 → msgraph_mcp_server-0.5.0}/src/msgraph_mcp/tools/util.py +0 -0
- {msgraph_mcp_server-0.3.2 → msgraph_mcp_server-0.5.0}/tests/__init__.py +0 -0
- {msgraph_mcp_server-0.3.2 → msgraph_mcp_server-0.5.0}/tests/conftest.py +0 -0
- {msgraph_mcp_server-0.3.2 → msgraph_mcp_server-0.5.0}/tests/fixtures/__init__.py +0 -0
- {msgraph_mcp_server-0.3.2 → msgraph_mcp_server-0.5.0}/tests/fixtures/message_minimal.json +0 -0
- {msgraph_mcp_server-0.3.2 → msgraph_mcp_server-0.5.0}/tests/integration/__init__.py +0 -0
- {msgraph_mcp_server-0.3.2 → msgraph_mcp_server-0.5.0}/tests/integration/test_live_smoke.py +0 -0
- {msgraph_mcp_server-0.3.2 → msgraph_mcp_server-0.5.0}/tests/integration/test_rules_and_folders_live.py +0 -0
- {msgraph_mcp_server-0.3.2 → msgraph_mcp_server-0.5.0}/tests/test_auth_cli.py +0 -0
- {msgraph_mcp_server-0.3.2 → msgraph_mcp_server-0.5.0}/tests/test_auth_msal_app.py +0 -0
- {msgraph_mcp_server-0.3.2 → msgraph_mcp_server-0.5.0}/tests/test_auth_token.py +0 -0
- {msgraph_mcp_server-0.3.2 → msgraph_mcp_server-0.5.0}/tests/test_config.py +0 -0
- {msgraph_mcp_server-0.3.2 → msgraph_mcp_server-0.5.0}/tests/test_graph_auth_provider.py +0 -0
- {msgraph_mcp_server-0.3.2 → msgraph_mcp_server-0.5.0}/tests/test_graph_batch.py +0 -0
- {msgraph_mcp_server-0.3.2 → msgraph_mcp_server-0.5.0}/tests/test_graph_client.py +0 -0
- {msgraph_mcp_server-0.3.2 → msgraph_mcp_server-0.5.0}/tests/test_graph_errors.py +0 -0
- {msgraph_mcp_server-0.3.2 → msgraph_mcp_server-0.5.0}/tests/test_graph_pagination.py +0 -0
- {msgraph_mcp_server-0.3.2 → msgraph_mcp_server-0.5.0}/tests/test_server.py +0 -0
- {msgraph_mcp_server-0.3.2 → msgraph_mcp_server-0.5.0}/tests/test_tools_mail_actions.py +0 -0
- {msgraph_mcp_server-0.3.2 → msgraph_mcp_server-0.5.0}/tests/test_tools_mail_batch.py +0 -0
- {msgraph_mcp_server-0.3.2 → msgraph_mcp_server-0.5.0}/tests/test_tools_mail_folders.py +0 -0
- {msgraph_mcp_server-0.3.2 → msgraph_mcp_server-0.5.0}/tests/test_tools_mail_read.py +0 -0
- {msgraph_mcp_server-0.3.2 → msgraph_mcp_server-0.5.0}/tests/test_tools_mail_rules.py +0 -0
- {msgraph_mcp_server-0.3.2 → msgraph_mcp_server-0.5.0}/tests/test_tools_mail_write.py +0 -0
- {msgraph_mcp_server-0.3.2 → msgraph_mcp_server-0.5.0}/tests/test_tools_teams_channels.py +0 -0
- {msgraph_mcp_server-0.3.2 → msgraph_mcp_server-0.5.0}/tests/test_tools_teams_chats.py +0 -0
- {msgraph_mcp_server-0.3.2 → msgraph_mcp_server-0.5.0}/tests/test_tools_teams_content.py +0 -0
- {msgraph_mcp_server-0.3.2 → msgraph_mcp_server-0.5.0}/tests/test_tools_util.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: msgraph-mcp-server
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: MCP server for Microsoft Graph: Outlook mail and calendar, plus read-only Microsoft Teams
|
|
5
5
|
Project-URL: Homepage, https://github.com/timfurlong/msgraph-mcp
|
|
6
6
|
Project-URL: Repository, https://github.com/timfurlong/msgraph-mcp
|
|
@@ -7,12 +7,12 @@
|
|
|
7
7
|
"url": "https://github.com/timfurlong/msgraph-mcp",
|
|
8
8
|
"source": "github"
|
|
9
9
|
},
|
|
10
|
-
"version": "0.
|
|
10
|
+
"version": "0.5.0",
|
|
11
11
|
"packages": [
|
|
12
12
|
{
|
|
13
13
|
"registryType": "pypi",
|
|
14
14
|
"identifier": "msgraph-mcp-server",
|
|
15
|
-
"version": "0.
|
|
15
|
+
"version": "0.5.0",
|
|
16
16
|
"runtimeHint": "uvx",
|
|
17
17
|
"transport": {
|
|
18
18
|
"type": "stdio"
|
|
@@ -188,6 +188,10 @@ def event_to_dict(evt: Any) -> dict:
|
|
|
188
188
|
"body": _body_to_dict(getattr(evt, "body", None)),
|
|
189
189
|
"showAs": _enum_value(getattr(evt, "show_as", None)),
|
|
190
190
|
"sensitivity": _enum_value(getattr(evt, "sensitivity", None)),
|
|
191
|
+
"isReminderOn": getattr(evt, "is_reminder_on", None),
|
|
192
|
+
"reminderMinutesBeforeStart": getattr(
|
|
193
|
+
evt, "reminder_minutes_before_start", None
|
|
194
|
+
),
|
|
191
195
|
"recurrence": getattr(evt, "recurrence", None),
|
|
192
196
|
"webLink": getattr(evt, "web_link", None),
|
|
193
197
|
}
|
|
@@ -101,6 +101,8 @@ def trim_event(raw: dict, *, include_body: bool, include_raw: bool) -> dict:
|
|
|
101
101
|
"body_preview": raw.get("bodyPreview"),
|
|
102
102
|
"show_as": raw.get("showAs"),
|
|
103
103
|
"sensitivity": raw.get("sensitivity"),
|
|
104
|
+
"is_reminder_on": raw.get("isReminderOn"),
|
|
105
|
+
"reminder_minutes_before_start": raw.get("reminderMinutesBeforeStart"),
|
|
104
106
|
"recurrence": raw.get("recurrence"),
|
|
105
107
|
"web_link": raw.get("webLink"),
|
|
106
108
|
}
|
|
@@ -13,6 +13,7 @@ from msgraph.generated.models.body_type import BodyType
|
|
|
13
13
|
from msgraph.generated.models.date_time_time_zone import DateTimeTimeZone
|
|
14
14
|
from msgraph.generated.models.email_address import EmailAddress
|
|
15
15
|
from msgraph.generated.models.event import Event
|
|
16
|
+
from msgraph.generated.models.free_busy_status import FreeBusyStatus
|
|
16
17
|
from msgraph.generated.models.item_body import ItemBody
|
|
17
18
|
from msgraph.generated.models.location import Location
|
|
18
19
|
from msgraph.generated.models.time_constraint import TimeConstraint
|
|
@@ -53,6 +54,16 @@ _VALID_RESPONSES = {
|
|
|
53
54
|
"decline": "declined",
|
|
54
55
|
}
|
|
55
56
|
|
|
57
|
+
ShowAs = Literal["free", "tentative", "busy", "oof", "workingElsewhere"]
|
|
58
|
+
|
|
59
|
+
_SHOW_AS = {
|
|
60
|
+
"free": FreeBusyStatus.Free,
|
|
61
|
+
"tentative": FreeBusyStatus.Tentative,
|
|
62
|
+
"busy": FreeBusyStatus.Busy,
|
|
63
|
+
"oof": FreeBusyStatus.Oof,
|
|
64
|
+
"workingElsewhere": FreeBusyStatus.WorkingElsewhere,
|
|
65
|
+
}
|
|
66
|
+
|
|
56
67
|
|
|
57
68
|
async def list_calendars(
|
|
58
69
|
*, graph, mailbox: str | None = None, include_raw: bool = False
|
|
@@ -170,6 +181,34 @@ def _dttz(dt: str, tz: str) -> DateTimeTimeZone:
|
|
|
170
181
|
return obj
|
|
171
182
|
|
|
172
183
|
|
|
184
|
+
def _free_busy(show_as: str) -> FreeBusyStatus:
|
|
185
|
+
if show_as not in _SHOW_AS:
|
|
186
|
+
raise GraphValidationError(f"show_as must be one of {sorted(_SHOW_AS)}")
|
|
187
|
+
return _SHOW_AS[show_as]
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _apply_reminder(
|
|
191
|
+
evt: Event,
|
|
192
|
+
*,
|
|
193
|
+
is_reminder_on: bool | None,
|
|
194
|
+
reminder_minutes_before_start: int | None,
|
|
195
|
+
) -> None:
|
|
196
|
+
"""Set reminder fields on evt. Giving minutes turns the reminder on."""
|
|
197
|
+
if reminder_minutes_before_start is not None:
|
|
198
|
+
if reminder_minutes_before_start < 0:
|
|
199
|
+
raise GraphValidationError(
|
|
200
|
+
"reminder_minutes_before_start must be zero or greater"
|
|
201
|
+
)
|
|
202
|
+
if is_reminder_on is False:
|
|
203
|
+
raise GraphValidationError(
|
|
204
|
+
"reminder_minutes_before_start cannot be set when is_reminder_on is False"
|
|
205
|
+
)
|
|
206
|
+
evt.reminder_minutes_before_start = reminder_minutes_before_start
|
|
207
|
+
evt.is_reminder_on = True
|
|
208
|
+
elif is_reminder_on is not None:
|
|
209
|
+
evt.is_reminder_on = is_reminder_on
|
|
210
|
+
|
|
211
|
+
|
|
173
212
|
def _build_event(
|
|
174
213
|
*,
|
|
175
214
|
subject: str,
|
|
@@ -182,6 +221,9 @@ def _build_event(
|
|
|
182
221
|
attendees: list[str] | None,
|
|
183
222
|
is_online_meeting: bool,
|
|
184
223
|
is_all_day: bool,
|
|
224
|
+
show_as: ShowAs | None,
|
|
225
|
+
is_reminder_on: bool | None,
|
|
226
|
+
reminder_minutes_before_start: int | None,
|
|
185
227
|
) -> Event:
|
|
186
228
|
e = Event()
|
|
187
229
|
e.subject = subject
|
|
@@ -208,6 +250,13 @@ def _build_event(
|
|
|
208
250
|
e.attendees = atts
|
|
209
251
|
e.is_online_meeting = is_online_meeting
|
|
210
252
|
e.is_all_day = is_all_day
|
|
253
|
+
if show_as is not None:
|
|
254
|
+
e.show_as = _free_busy(show_as)
|
|
255
|
+
_apply_reminder(
|
|
256
|
+
e,
|
|
257
|
+
is_reminder_on=is_reminder_on,
|
|
258
|
+
reminder_minutes_before_start=reminder_minutes_before_start,
|
|
259
|
+
)
|
|
211
260
|
return e
|
|
212
261
|
|
|
213
262
|
|
|
@@ -224,6 +273,9 @@ async def create_event(
|
|
|
224
273
|
attendees: list[str] | None = None,
|
|
225
274
|
is_online_meeting: bool = False,
|
|
226
275
|
is_all_day: bool = False,
|
|
276
|
+
show_as: ShowAs | None = None,
|
|
277
|
+
is_reminder_on: bool | None = None,
|
|
278
|
+
reminder_minutes_before_start: int | None = None,
|
|
227
279
|
calendar_id: str | None = None,
|
|
228
280
|
mailbox: str | None = None,
|
|
229
281
|
include_raw: bool = False,
|
|
@@ -240,6 +292,12 @@ async def create_event(
|
|
|
240
292
|
attendees: Optional list of attendee email addresses (treated as required).
|
|
241
293
|
is_online_meeting: When True, Outlook adds a Teams meeting link.
|
|
242
294
|
is_all_day: All-day event flag.
|
|
295
|
+
show_as: Free/busy status shown to others: "free", "tentative", "busy",
|
|
296
|
+
"oof", or "workingElsewhere". Omit for Outlook's default ("busy").
|
|
297
|
+
is_reminder_on: False creates the event with no reminder. Omit for
|
|
298
|
+
Outlook's default (a reminder 15 minutes before start).
|
|
299
|
+
reminder_minutes_before_start: Minutes before start to alert (0 or
|
|
300
|
+
more). Setting this turns the reminder on.
|
|
243
301
|
calendar_id: Optional calendar id; default is the user's primary calendar.
|
|
244
302
|
mailbox: Optional mailbox.
|
|
245
303
|
|
|
@@ -257,6 +315,9 @@ async def create_event(
|
|
|
257
315
|
attendees=attendees,
|
|
258
316
|
is_online_meeting=is_online_meeting,
|
|
259
317
|
is_all_day=is_all_day,
|
|
318
|
+
show_as=show_as,
|
|
319
|
+
is_reminder_on=is_reminder_on,
|
|
320
|
+
reminder_minutes_before_start=reminder_minutes_before_start,
|
|
260
321
|
)
|
|
261
322
|
try:
|
|
262
323
|
target = graph.mailbox(mailbox)
|
|
@@ -285,12 +346,22 @@ async def update_event(
|
|
|
285
346
|
location: str | None = None,
|
|
286
347
|
is_online_meeting: bool | None = None,
|
|
287
348
|
is_all_day: bool | None = None,
|
|
349
|
+
show_as: ShowAs | None = None,
|
|
350
|
+
is_reminder_on: bool | None = None,
|
|
351
|
+
reminder_minutes_before_start: int | None = None,
|
|
288
352
|
mailbox: str | None = None,
|
|
289
353
|
include_raw: bool = False,
|
|
290
354
|
) -> dict:
|
|
291
355
|
"""Patch fields of an existing event. Pass None to leave a field unchanged.
|
|
292
356
|
|
|
293
357
|
If you change start_datetime or end_datetime, you must also pass time_zone.
|
|
358
|
+
|
|
359
|
+
show_as sets the free/busy status shown to others: "free", "tentative",
|
|
360
|
+
"busy", "oof", or "workingElsewhere".
|
|
361
|
+
|
|
362
|
+
is_reminder_on=False removes the reminder. reminder_minutes_before_start
|
|
363
|
+
sets how many minutes before start to alert (0 or more) and turns the
|
|
364
|
+
reminder on.
|
|
294
365
|
"""
|
|
295
366
|
if (start_datetime or end_datetime) and not time_zone:
|
|
296
367
|
raise GraphValidationError("time_zone is required when updating start or end")
|
|
@@ -314,6 +385,13 @@ async def update_event(
|
|
|
314
385
|
patch.is_online_meeting = is_online_meeting
|
|
315
386
|
if is_all_day is not None:
|
|
316
387
|
patch.is_all_day = is_all_day
|
|
388
|
+
if show_as is not None:
|
|
389
|
+
patch.show_as = _free_busy(show_as)
|
|
390
|
+
_apply_reminder(
|
|
391
|
+
patch,
|
|
392
|
+
is_reminder_on=is_reminder_on,
|
|
393
|
+
reminder_minutes_before_start=reminder_minutes_before_start,
|
|
394
|
+
)
|
|
317
395
|
try:
|
|
318
396
|
updated = await graph.mailbox(mailbox).events.by_event_id(event_id).patch(patch)
|
|
319
397
|
except NotAuthenticatedError:
|
|
@@ -547,6 +625,9 @@ def register(mcp, *, graph) -> None:
|
|
|
547
625
|
attendees: list[str] | None = None,
|
|
548
626
|
is_online_meeting: bool = False,
|
|
549
627
|
is_all_day: bool = False,
|
|
628
|
+
show_as: ShowAs | None = None,
|
|
629
|
+
is_reminder_on: bool | None = None,
|
|
630
|
+
reminder_minutes_before_start: int | None = None,
|
|
550
631
|
calendar_id: str | None = None,
|
|
551
632
|
mailbox: str | None = None,
|
|
552
633
|
include_raw: bool = False,
|
|
@@ -563,6 +644,9 @@ def register(mcp, *, graph) -> None:
|
|
|
563
644
|
attendees=attendees,
|
|
564
645
|
is_online_meeting=is_online_meeting,
|
|
565
646
|
is_all_day=is_all_day,
|
|
647
|
+
show_as=show_as,
|
|
648
|
+
is_reminder_on=is_reminder_on,
|
|
649
|
+
reminder_minutes_before_start=reminder_minutes_before_start,
|
|
566
650
|
calendar_id=calendar_id,
|
|
567
651
|
mailbox=mailbox,
|
|
568
652
|
include_raw=include_raw,
|
|
@@ -580,6 +664,9 @@ def register(mcp, *, graph) -> None:
|
|
|
580
664
|
location: str | None = None,
|
|
581
665
|
is_online_meeting: bool | None = None,
|
|
582
666
|
is_all_day: bool | None = None,
|
|
667
|
+
show_as: ShowAs | None = None,
|
|
668
|
+
is_reminder_on: bool | None = None,
|
|
669
|
+
reminder_minutes_before_start: int | None = None,
|
|
583
670
|
mailbox: str | None = None,
|
|
584
671
|
include_raw: bool = False,
|
|
585
672
|
):
|
|
@@ -595,6 +682,9 @@ def register(mcp, *, graph) -> None:
|
|
|
595
682
|
location=location,
|
|
596
683
|
is_online_meeting=is_online_meeting,
|
|
597
684
|
is_all_day=is_all_day,
|
|
685
|
+
show_as=show_as,
|
|
686
|
+
is_reminder_on=is_reminder_on,
|
|
687
|
+
reminder_minutes_before_start=reminder_minutes_before_start,
|
|
598
688
|
mailbox=mailbox,
|
|
599
689
|
include_raw=include_raw,
|
|
600
690
|
)
|
|
@@ -20,6 +20,8 @@
|
|
|
20
20
|
"body": {"contentType": "html", "content": "<p>Daily standup</p>"},
|
|
21
21
|
"showAs": "busy",
|
|
22
22
|
"sensitivity": "normal",
|
|
23
|
+
"isReminderOn": true,
|
|
24
|
+
"reminderMinutesBeforeStart": 15,
|
|
23
25
|
"recurrence": null,
|
|
24
26
|
"webLink": "https://outlook.office.com/owa/?ItemID=evt"
|
|
25
27
|
}
|
|
@@ -151,6 +151,18 @@ def test_message_rule_predicates_to_dict_minimal():
|
|
|
151
151
|
assert d["fromAddresses"] == []
|
|
152
152
|
|
|
153
153
|
|
|
154
|
+
def test_event_to_dict_includes_reminder():
|
|
155
|
+
evt = SimpleNamespace(
|
|
156
|
+
id="e1",
|
|
157
|
+
is_reminder_on=True,
|
|
158
|
+
reminder_minutes_before_start=30,
|
|
159
|
+
additional_data={},
|
|
160
|
+
)
|
|
161
|
+
d = serialize.event_to_dict(evt)
|
|
162
|
+
assert d["isReminderOn"] is True
|
|
163
|
+
assert d["reminderMinutesBeforeStart"] == 30
|
|
164
|
+
|
|
165
|
+
|
|
154
166
|
def test_message_rule_actions_to_dict_minimal():
|
|
155
167
|
actions = SimpleNamespace(
|
|
156
168
|
assign_categories=None,
|
|
@@ -84,6 +84,13 @@ def test_trim_event_minimal():
|
|
|
84
84
|
assert "body" not in t
|
|
85
85
|
|
|
86
86
|
|
|
87
|
+
def test_trim_event_includes_reminder():
|
|
88
|
+
raw = load("event_minimal.json")
|
|
89
|
+
t = trimming.trim_event(raw, include_body=False, include_raw=False)
|
|
90
|
+
assert t["is_reminder_on"] is True
|
|
91
|
+
assert t["reminder_minutes_before_start"] == 15
|
|
92
|
+
|
|
93
|
+
|
|
87
94
|
def test_trim_event_with_body_and_raw():
|
|
88
95
|
raw = load("event_minimal.json")
|
|
89
96
|
t = trimming.trim_event(raw, include_body=True, include_raw=True)
|
|
@@ -2,6 +2,7 @@ from types import SimpleNamespace
|
|
|
2
2
|
from unittest.mock import AsyncMock, MagicMock
|
|
3
3
|
|
|
4
4
|
import pytest
|
|
5
|
+
from msgraph.generated.models.free_busy_status import FreeBusyStatus
|
|
5
6
|
|
|
6
7
|
from msgraph_mcp.graph.errors import GraphValidationError
|
|
7
8
|
from msgraph_mcp.tools import calendar as cal_tools
|
|
@@ -134,6 +135,199 @@ async def test_update_event_patches():
|
|
|
134
135
|
assert result["id"] == "e1"
|
|
135
136
|
|
|
136
137
|
|
|
138
|
+
@pytest.mark.asyncio
|
|
139
|
+
async def test_create_event_sets_show_as():
|
|
140
|
+
graph = MagicMock()
|
|
141
|
+
mb = MagicMock()
|
|
142
|
+
mb.events.post = AsyncMock(return_value=_fake_event(id_="new1"))
|
|
143
|
+
graph.mailbox = MagicMock(return_value=mb)
|
|
144
|
+
|
|
145
|
+
await cal_tools.create_event(
|
|
146
|
+
graph=graph,
|
|
147
|
+
subject="Focus time",
|
|
148
|
+
start_datetime="2026-05-19T15:00:00",
|
|
149
|
+
end_datetime="2026-05-19T15:30:00",
|
|
150
|
+
show_as="free",
|
|
151
|
+
)
|
|
152
|
+
assert mb.events.post.await_args is not None
|
|
153
|
+
posted = mb.events.post.await_args.args[0]
|
|
154
|
+
assert posted.show_as == FreeBusyStatus.Free
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
@pytest.mark.asyncio
|
|
158
|
+
async def test_create_event_leaves_show_as_unset_by_default():
|
|
159
|
+
graph = MagicMock()
|
|
160
|
+
mb = MagicMock()
|
|
161
|
+
mb.events.post = AsyncMock(return_value=_fake_event(id_="new1"))
|
|
162
|
+
graph.mailbox = MagicMock(return_value=mb)
|
|
163
|
+
|
|
164
|
+
await cal_tools.create_event(
|
|
165
|
+
graph=graph,
|
|
166
|
+
subject="Standup",
|
|
167
|
+
start_datetime="2026-05-19T15:00:00",
|
|
168
|
+
end_datetime="2026-05-19T15:30:00",
|
|
169
|
+
)
|
|
170
|
+
assert mb.events.post.await_args is not None
|
|
171
|
+
posted = mb.events.post.await_args.args[0]
|
|
172
|
+
assert posted.show_as is None
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
@pytest.mark.asyncio
|
|
176
|
+
async def test_create_event_rejects_bad_show_as():
|
|
177
|
+
graph = MagicMock()
|
|
178
|
+
with pytest.raises(GraphValidationError):
|
|
179
|
+
await cal_tools.create_event(
|
|
180
|
+
graph=graph,
|
|
181
|
+
subject="Standup",
|
|
182
|
+
start_datetime="2026-05-19T15:00:00",
|
|
183
|
+
end_datetime="2026-05-19T15:30:00",
|
|
184
|
+
show_as="available", # type: ignore[arg-type]
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
@pytest.mark.asyncio
|
|
189
|
+
async def test_update_event_patches_show_as():
|
|
190
|
+
graph = MagicMock()
|
|
191
|
+
mb = MagicMock()
|
|
192
|
+
evt_builder = MagicMock(patch=AsyncMock(return_value=_fake_event(id_="e1")))
|
|
193
|
+
mb.events.by_event_id = MagicMock(return_value=evt_builder)
|
|
194
|
+
graph.mailbox = MagicMock(return_value=mb)
|
|
195
|
+
|
|
196
|
+
await cal_tools.update_event(graph=graph, event_id="e1", show_as="workingElsewhere")
|
|
197
|
+
patched = evt_builder.patch.await_args.args[0]
|
|
198
|
+
assert patched.show_as == FreeBusyStatus.WorkingElsewhere
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
@pytest.mark.asyncio
|
|
202
|
+
async def test_update_event_rejects_bad_show_as():
|
|
203
|
+
graph = MagicMock()
|
|
204
|
+
with pytest.raises(GraphValidationError):
|
|
205
|
+
await cal_tools.update_event(
|
|
206
|
+
graph=graph,
|
|
207
|
+
event_id="e1",
|
|
208
|
+
show_as="available", # type: ignore[arg-type]
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def test_register_exposes_show_as_on_create_and_update():
|
|
213
|
+
import inspect
|
|
214
|
+
|
|
215
|
+
registered: dict = {}
|
|
216
|
+
|
|
217
|
+
class FakeMCP:
|
|
218
|
+
def tool(self, *, name, description=""): # noqa: ARG002
|
|
219
|
+
def deco(fn):
|
|
220
|
+
registered[name] = fn
|
|
221
|
+
return fn
|
|
222
|
+
|
|
223
|
+
return deco
|
|
224
|
+
|
|
225
|
+
cal_tools.register(FakeMCP(), graph=MagicMock())
|
|
226
|
+
|
|
227
|
+
for name in ("create_event", "update_event"):
|
|
228
|
+
assert "show_as" in inspect.signature(registered[name]).parameters
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
async def _create_with(**kwargs):
|
|
232
|
+
graph = MagicMock()
|
|
233
|
+
mb = MagicMock()
|
|
234
|
+
mb.events.post = AsyncMock(return_value=_fake_event(id_="new1"))
|
|
235
|
+
graph.mailbox = MagicMock(return_value=mb)
|
|
236
|
+
await cal_tools.create_event(
|
|
237
|
+
graph=graph,
|
|
238
|
+
subject="Standup",
|
|
239
|
+
start_datetime="2026-05-19T15:00:00",
|
|
240
|
+
end_datetime="2026-05-19T15:30:00",
|
|
241
|
+
**kwargs,
|
|
242
|
+
)
|
|
243
|
+
assert mb.events.post.await_args is not None
|
|
244
|
+
return mb.events.post.await_args.args[0]
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
async def _update_with(**kwargs):
|
|
248
|
+
graph = MagicMock()
|
|
249
|
+
mb = MagicMock()
|
|
250
|
+
evt_builder = MagicMock(patch=AsyncMock(return_value=_fake_event(id_="e1")))
|
|
251
|
+
mb.events.by_event_id = MagicMock(return_value=evt_builder)
|
|
252
|
+
graph.mailbox = MagicMock(return_value=mb)
|
|
253
|
+
await cal_tools.update_event(graph=graph, event_id="e1", **kwargs)
|
|
254
|
+
return evt_builder.patch.await_args.args[0]
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
@pytest.mark.asyncio
|
|
258
|
+
async def test_create_event_can_turn_reminder_off():
|
|
259
|
+
posted = await _create_with(is_reminder_on=False)
|
|
260
|
+
assert posted.is_reminder_on is False
|
|
261
|
+
assert posted.reminder_minutes_before_start is None
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
@pytest.mark.asyncio
|
|
265
|
+
async def test_create_event_reminder_minutes_turns_reminder_on():
|
|
266
|
+
posted = await _create_with(reminder_minutes_before_start=60)
|
|
267
|
+
assert posted.reminder_minutes_before_start == 60
|
|
268
|
+
assert posted.is_reminder_on is True
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
@pytest.mark.asyncio
|
|
272
|
+
async def test_create_event_leaves_reminder_unset_by_default():
|
|
273
|
+
posted = await _create_with()
|
|
274
|
+
assert posted.is_reminder_on is None
|
|
275
|
+
assert posted.reminder_minutes_before_start is None
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
@pytest.mark.asyncio
|
|
279
|
+
async def test_create_event_rejects_negative_reminder_minutes():
|
|
280
|
+
with pytest.raises(GraphValidationError):
|
|
281
|
+
await _create_with(reminder_minutes_before_start=-5)
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
@pytest.mark.asyncio
|
|
285
|
+
async def test_create_event_rejects_minutes_with_reminder_off():
|
|
286
|
+
with pytest.raises(GraphValidationError):
|
|
287
|
+
await _create_with(is_reminder_on=False, reminder_minutes_before_start=10)
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
@pytest.mark.asyncio
|
|
291
|
+
async def test_update_event_can_turn_reminder_off():
|
|
292
|
+
patched = await _update_with(is_reminder_on=False)
|
|
293
|
+
assert patched.is_reminder_on is False
|
|
294
|
+
assert patched.reminder_minutes_before_start is None
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
@pytest.mark.asyncio
|
|
298
|
+
async def test_update_event_reminder_minutes_turns_reminder_on():
|
|
299
|
+
patched = await _update_with(reminder_minutes_before_start=0)
|
|
300
|
+
assert patched.reminder_minutes_before_start == 0
|
|
301
|
+
assert patched.is_reminder_on is True
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
@pytest.mark.asyncio
|
|
305
|
+
async def test_update_event_rejects_negative_reminder_minutes():
|
|
306
|
+
with pytest.raises(GraphValidationError):
|
|
307
|
+
await _update_with(reminder_minutes_before_start=-1)
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def test_register_exposes_reminder_params_on_create_and_update():
|
|
311
|
+
import inspect
|
|
312
|
+
|
|
313
|
+
registered: dict = {}
|
|
314
|
+
|
|
315
|
+
class FakeMCP:
|
|
316
|
+
def tool(self, *, name, description=""): # noqa: ARG002
|
|
317
|
+
def deco(fn):
|
|
318
|
+
registered[name] = fn
|
|
319
|
+
return fn
|
|
320
|
+
|
|
321
|
+
return deco
|
|
322
|
+
|
|
323
|
+
cal_tools.register(FakeMCP(), graph=MagicMock())
|
|
324
|
+
|
|
325
|
+
for name in ("create_event", "update_event"):
|
|
326
|
+
params = inspect.signature(registered[name]).parameters
|
|
327
|
+
assert "is_reminder_on" in params
|
|
328
|
+
assert "reminder_minutes_before_start" in params
|
|
329
|
+
|
|
330
|
+
|
|
137
331
|
@pytest.mark.asyncio
|
|
138
332
|
async def test_delete_event_calls_delete():
|
|
139
333
|
graph = MagicMock()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{msgraph_mcp_server-0.3.2 → msgraph_mcp_server-0.5.0}/src/msgraph_mcp/graph/auth_provider.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{msgraph_mcp_server-0.3.2 → msgraph_mcp_server-0.5.0}/src/msgraph_mcp/tools/teams_channels.py
RENAMED
|
File without changes
|
|
File without changes
|
{msgraph_mcp_server-0.3.2 → msgraph_mcp_server-0.5.0}/src/msgraph_mcp/tools/teams_content.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|