msgraph-mcp-server 0.4.0__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.
Files changed (71) hide show
  1. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.0}/PKG-INFO +1 -1
  2. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.0}/pyproject.toml +1 -1
  3. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.0}/server.json +2 -2
  4. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.0}/src/msgraph_mcp/graph/serialize.py +4 -0
  5. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.0}/src/msgraph_mcp/graph/trimming.py +2 -0
  6. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.0}/src/msgraph_mcp/tools/calendar.py +56 -0
  7. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.0}/tests/fixtures/event_minimal.json +2 -0
  8. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.0}/tests/test_graph_serialize.py +12 -0
  9. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.0}/tests/test_graph_trimming.py +7 -0
  10. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.0}/tests/test_tools_calendar.py +100 -0
  11. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.0}/uv.lock +1 -1
  12. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.0}/.env.example +0 -0
  13. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.0}/.github/dependabot.yml +0 -0
  14. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.0}/.github/workflows/ci.yml +0 -0
  15. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.0}/.github/workflows/release.yml +0 -0
  16. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.0}/.gitignore +0 -0
  17. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.0}/CONTRIBUTING.md +0 -0
  18. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.0}/LICENSE +0 -0
  19. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.0}/README.md +0 -0
  20. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.0}/src/msgraph_mcp/__init__.py +0 -0
  21. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.0}/src/msgraph_mcp/auth/__init__.py +0 -0
  22. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.0}/src/msgraph_mcp/auth/cli.py +0 -0
  23. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.0}/src/msgraph_mcp/auth/msal_app.py +0 -0
  24. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.0}/src/msgraph_mcp/auth/token.py +0 -0
  25. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.0}/src/msgraph_mcp/config.py +0 -0
  26. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.0}/src/msgraph_mcp/graph/__init__.py +0 -0
  27. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.0}/src/msgraph_mcp/graph/auth_provider.py +0 -0
  28. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.0}/src/msgraph_mcp/graph/batch.py +0 -0
  29. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.0}/src/msgraph_mcp/graph/client.py +0 -0
  30. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.0}/src/msgraph_mcp/graph/errors.py +0 -0
  31. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.0}/src/msgraph_mcp/graph/pagination.py +0 -0
  32. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.0}/src/msgraph_mcp/server.py +0 -0
  33. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.0}/src/msgraph_mcp/tools/__init__.py +0 -0
  34. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.0}/src/msgraph_mcp/tools/_binary.py +0 -0
  35. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.0}/src/msgraph_mcp/tools/mail_actions.py +0 -0
  36. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.0}/src/msgraph_mcp/tools/mail_batch.py +0 -0
  37. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.0}/src/msgraph_mcp/tools/mail_folders.py +0 -0
  38. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.0}/src/msgraph_mcp/tools/mail_read.py +0 -0
  39. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.0}/src/msgraph_mcp/tools/mail_rules.py +0 -0
  40. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.0}/src/msgraph_mcp/tools/mail_write.py +0 -0
  41. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.0}/src/msgraph_mcp/tools/teams_channels.py +0 -0
  42. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.0}/src/msgraph_mcp/tools/teams_chats.py +0 -0
  43. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.0}/src/msgraph_mcp/tools/teams_content.py +0 -0
  44. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.0}/src/msgraph_mcp/tools/util.py +0 -0
  45. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.0}/tests/__init__.py +0 -0
  46. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.0}/tests/conftest.py +0 -0
  47. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.0}/tests/fixtures/__init__.py +0 -0
  48. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.0}/tests/fixtures/message_minimal.json +0 -0
  49. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.0}/tests/integration/__init__.py +0 -0
  50. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.0}/tests/integration/test_live_smoke.py +0 -0
  51. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.0}/tests/integration/test_rules_and_folders_live.py +0 -0
  52. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.0}/tests/test_auth_cli.py +0 -0
  53. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.0}/tests/test_auth_msal_app.py +0 -0
  54. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.0}/tests/test_auth_token.py +0 -0
  55. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.0}/tests/test_config.py +0 -0
  56. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.0}/tests/test_graph_auth_provider.py +0 -0
  57. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.0}/tests/test_graph_batch.py +0 -0
  58. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.0}/tests/test_graph_client.py +0 -0
  59. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.0}/tests/test_graph_errors.py +0 -0
  60. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.0}/tests/test_graph_pagination.py +0 -0
  61. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.0}/tests/test_server.py +0 -0
  62. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.0}/tests/test_tools_mail_actions.py +0 -0
  63. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.0}/tests/test_tools_mail_batch.py +0 -0
  64. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.0}/tests/test_tools_mail_folders.py +0 -0
  65. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.0}/tests/test_tools_mail_read.py +0 -0
  66. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.0}/tests/test_tools_mail_rules.py +0 -0
  67. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.0}/tests/test_tools_mail_write.py +0 -0
  68. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.0}/tests/test_tools_teams_channels.py +0 -0
  69. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.0}/tests/test_tools_teams_chats.py +0 -0
  70. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.0}/tests/test_tools_teams_content.py +0 -0
  71. {msgraph_mcp_server-0.4.0 → 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.4.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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "msgraph-mcp-server"
3
- version = "0.4.0"
3
+ version = "0.5.0"
4
4
  description = "MCP server for Microsoft Graph: Outlook mail and calendar, plus read-only Microsoft Teams"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -7,12 +7,12 @@
7
7
  "url": "https://github.com/timfurlong/msgraph-mcp",
8
8
  "source": "github"
9
9
  },
10
- "version": "0.4.0",
10
+ "version": "0.5.0",
11
11
  "packages": [
12
12
  {
13
13
  "registryType": "pypi",
14
14
  "identifier": "msgraph-mcp-server",
15
- "version": "0.4.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
  }
@@ -187,6 +187,28 @@ def _free_busy(show_as: str) -> FreeBusyStatus:
187
187
  return _SHOW_AS[show_as]
188
188
 
189
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
+
190
212
  def _build_event(
191
213
  *,
192
214
  subject: str,
@@ -200,6 +222,8 @@ def _build_event(
200
222
  is_online_meeting: bool,
201
223
  is_all_day: bool,
202
224
  show_as: ShowAs | None,
225
+ is_reminder_on: bool | None,
226
+ reminder_minutes_before_start: int | None,
203
227
  ) -> Event:
204
228
  e = Event()
205
229
  e.subject = subject
@@ -228,6 +252,11 @@ def _build_event(
228
252
  e.is_all_day = is_all_day
229
253
  if show_as is not None:
230
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
+ )
231
260
  return e
232
261
 
233
262
 
@@ -245,6 +274,8 @@ async def create_event(
245
274
  is_online_meeting: bool = False,
246
275
  is_all_day: bool = False,
247
276
  show_as: ShowAs | None = None,
277
+ is_reminder_on: bool | None = None,
278
+ reminder_minutes_before_start: int | None = None,
248
279
  calendar_id: str | None = None,
249
280
  mailbox: str | None = None,
250
281
  include_raw: bool = False,
@@ -263,6 +294,10 @@ async def create_event(
263
294
  is_all_day: All-day event flag.
264
295
  show_as: Free/busy status shown to others: "free", "tentative", "busy",
265
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.
266
301
  calendar_id: Optional calendar id; default is the user's primary calendar.
267
302
  mailbox: Optional mailbox.
268
303
 
@@ -281,6 +316,8 @@ async def create_event(
281
316
  is_online_meeting=is_online_meeting,
282
317
  is_all_day=is_all_day,
283
318
  show_as=show_as,
319
+ is_reminder_on=is_reminder_on,
320
+ reminder_minutes_before_start=reminder_minutes_before_start,
284
321
  )
285
322
  try:
286
323
  target = graph.mailbox(mailbox)
@@ -310,6 +347,8 @@ async def update_event(
310
347
  is_online_meeting: bool | None = None,
311
348
  is_all_day: bool | None = None,
312
349
  show_as: ShowAs | None = None,
350
+ is_reminder_on: bool | None = None,
351
+ reminder_minutes_before_start: int | None = None,
313
352
  mailbox: str | None = None,
314
353
  include_raw: bool = False,
315
354
  ) -> dict:
@@ -319,6 +358,10 @@ async def update_event(
319
358
 
320
359
  show_as sets the free/busy status shown to others: "free", "tentative",
321
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.
322
365
  """
323
366
  if (start_datetime or end_datetime) and not time_zone:
324
367
  raise GraphValidationError("time_zone is required when updating start or end")
@@ -344,6 +387,11 @@ async def update_event(
344
387
  patch.is_all_day = is_all_day
345
388
  if show_as is not None:
346
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
+ )
347
395
  try:
348
396
  updated = await graph.mailbox(mailbox).events.by_event_id(event_id).patch(patch)
349
397
  except NotAuthenticatedError:
@@ -578,6 +626,8 @@ def register(mcp, *, graph) -> None:
578
626
  is_online_meeting: bool = False,
579
627
  is_all_day: bool = False,
580
628
  show_as: ShowAs | None = None,
629
+ is_reminder_on: bool | None = None,
630
+ reminder_minutes_before_start: int | None = None,
581
631
  calendar_id: str | None = None,
582
632
  mailbox: str | None = None,
583
633
  include_raw: bool = False,
@@ -595,6 +645,8 @@ def register(mcp, *, graph) -> None:
595
645
  is_online_meeting=is_online_meeting,
596
646
  is_all_day=is_all_day,
597
647
  show_as=show_as,
648
+ is_reminder_on=is_reminder_on,
649
+ reminder_minutes_before_start=reminder_minutes_before_start,
598
650
  calendar_id=calendar_id,
599
651
  mailbox=mailbox,
600
652
  include_raw=include_raw,
@@ -613,6 +665,8 @@ def register(mcp, *, graph) -> None:
613
665
  is_online_meeting: bool | None = None,
614
666
  is_all_day: bool | None = None,
615
667
  show_as: ShowAs | None = None,
668
+ is_reminder_on: bool | None = None,
669
+ reminder_minutes_before_start: int | None = None,
616
670
  mailbox: str | None = None,
617
671
  include_raw: bool = False,
618
672
  ):
@@ -629,6 +683,8 @@ def register(mcp, *, graph) -> None:
629
683
  is_online_meeting=is_online_meeting,
630
684
  is_all_day=is_all_day,
631
685
  show_as=show_as,
686
+ is_reminder_on=is_reminder_on,
687
+ reminder_minutes_before_start=reminder_minutes_before_start,
632
688
  mailbox=mailbox,
633
689
  include_raw=include_raw,
634
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)
@@ -228,6 +228,106 @@ def test_register_exposes_show_as_on_create_and_update():
228
228
  assert "show_as" in inspect.signature(registered[name]).parameters
229
229
 
230
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
+
231
331
  @pytest.mark.asyncio
232
332
  async def test_delete_event_calls_delete():
233
333
  graph = MagicMock()
@@ -827,7 +827,7 @@ wheels = [
827
827
 
828
828
  [[package]]
829
829
  name = "msgraph-mcp-server"
830
- version = "0.4.0"
830
+ version = "0.5.0"
831
831
  source = { editable = "." }
832
832
  dependencies = [
833
833
  { name = "mcp" },