msgraph-mcp-server 0.4.0__tar.gz → 0.5.1__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 (73) hide show
  1. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.1}/.github/dependabot.yml +6 -0
  2. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.1}/PKG-INFO +3 -2
  3. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.1}/pyproject.toml +9 -2
  4. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.1}/server.json +2 -2
  5. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.1}/src/msgraph_mcp/config.py +0 -1
  6. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.1}/src/msgraph_mcp/graph/auth_provider.py +0 -1
  7. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.1}/src/msgraph_mcp/graph/batch.py +3 -1
  8. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.1}/src/msgraph_mcp/graph/pagination.py +0 -1
  9. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.1}/src/msgraph_mcp/graph/serialize.py +4 -0
  10. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.1}/src/msgraph_mcp/graph/trimming.py +2 -0
  11. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.1}/src/msgraph_mcp/server.py +5 -6
  12. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.1}/src/msgraph_mcp/tools/__init__.py +2 -2
  13. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.1}/src/msgraph_mcp/tools/_binary.py +3 -3
  14. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.1}/src/msgraph_mcp/tools/calendar.py +65 -10
  15. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.1}/src/msgraph_mcp/tools/mail_folders.py +6 -6
  16. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.1}/src/msgraph_mcp/tools/mail_read.py +5 -5
  17. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.1}/src/msgraph_mcp/tools/mail_rules.py +5 -5
  18. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.1}/src/msgraph_mcp/tools/mail_write.py +7 -8
  19. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.1}/src/msgraph_mcp/tools/teams_channels.py +8 -7
  20. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.1}/src/msgraph_mcp/tools/teams_chats.py +2 -2
  21. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.1}/src/msgraph_mcp/tools/teams_content.py +1 -1
  22. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.1}/src/msgraph_mcp/tools/util.py +1 -1
  23. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.1}/tests/fixtures/event_minimal.json +2 -0
  24. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.1}/tests/integration/test_live_smoke.py +2 -1
  25. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.1}/tests/test_graph_batch.py +1 -1
  26. msgraph_mcp_server-0.5.1/tests/test_graph_me_url.py +36 -0
  27. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.1}/tests/test_graph_pagination.py +2 -0
  28. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.1}/tests/test_graph_serialize.py +13 -1
  29. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.1}/tests/test_graph_trimming.py +11 -5
  30. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.1}/tests/test_server.py +4 -5
  31. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.1}/tests/test_tools_calendar.py +101 -1
  32. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.1}/tests/test_tools_mail_batch.py +1 -1
  33. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.1}/tests/test_tools_mail_folders.py +2 -2
  34. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.1}/tests/test_tools_mail_read.py +2 -2
  35. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.1}/tests/test_tools_mail_rules.py +9 -9
  36. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.1}/tests/test_tools_teams_content.py +2 -2
  37. msgraph_mcp_server-0.5.1/uv.lock +2004 -0
  38. msgraph_mcp_server-0.4.0/uv.lock +0 -1776
  39. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.1}/.env.example +0 -0
  40. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.1}/.github/workflows/ci.yml +0 -0
  41. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.1}/.github/workflows/release.yml +0 -0
  42. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.1}/.gitignore +0 -0
  43. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.1}/CONTRIBUTING.md +0 -0
  44. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.1}/LICENSE +0 -0
  45. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.1}/README.md +0 -0
  46. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.1}/src/msgraph_mcp/__init__.py +0 -0
  47. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.1}/src/msgraph_mcp/auth/__init__.py +0 -0
  48. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.1}/src/msgraph_mcp/auth/cli.py +0 -0
  49. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.1}/src/msgraph_mcp/auth/msal_app.py +0 -0
  50. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.1}/src/msgraph_mcp/auth/token.py +0 -0
  51. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.1}/src/msgraph_mcp/graph/__init__.py +0 -0
  52. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.1}/src/msgraph_mcp/graph/client.py +0 -0
  53. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.1}/src/msgraph_mcp/graph/errors.py +0 -0
  54. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.1}/src/msgraph_mcp/tools/mail_actions.py +0 -0
  55. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.1}/src/msgraph_mcp/tools/mail_batch.py +0 -0
  56. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.1}/tests/__init__.py +0 -0
  57. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.1}/tests/conftest.py +0 -0
  58. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.1}/tests/fixtures/__init__.py +0 -0
  59. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.1}/tests/fixtures/message_minimal.json +0 -0
  60. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.1}/tests/integration/__init__.py +0 -0
  61. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.1}/tests/integration/test_rules_and_folders_live.py +0 -0
  62. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.1}/tests/test_auth_cli.py +0 -0
  63. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.1}/tests/test_auth_msal_app.py +0 -0
  64. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.1}/tests/test_auth_token.py +0 -0
  65. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.1}/tests/test_config.py +0 -0
  66. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.1}/tests/test_graph_auth_provider.py +0 -0
  67. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.1}/tests/test_graph_client.py +0 -0
  68. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.1}/tests/test_graph_errors.py +0 -0
  69. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.1}/tests/test_tools_mail_actions.py +0 -0
  70. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.1}/tests/test_tools_mail_write.py +0 -0
  71. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.1}/tests/test_tools_teams_channels.py +0 -0
  72. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.1}/tests/test_tools_teams_chats.py +0 -0
  73. {msgraph_mcp_server-0.4.0 → msgraph_mcp_server-0.5.1}/tests/test_tools_util.py +0 -0
@@ -7,6 +7,12 @@ updates:
7
7
  groups:
8
8
  python-dependencies:
9
9
  patterns: ["*"]
10
+ # Groups only cover version updates unless told otherwise, so security
11
+ # fixes used to arrive as one PR per package, each rewriting uv.lock and
12
+ # conflicting with the rest.
13
+ python-security:
14
+ applies-to: security-updates
15
+ patterns: ["*"]
10
16
 
11
17
  - package-ecosystem: github-actions
12
18
  directory: /
@@ -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.1
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
@@ -19,7 +19,8 @@ Classifier: Programming Language :: Python :: 3.13
19
19
  Classifier: Topic :: Communications :: Email
20
20
  Classifier: Topic :: Office/Business :: Scheduling
21
21
  Requires-Python: >=3.11
22
- Requires-Dist: mcp>=1.0
22
+ Requires-Dist: mcp<3,>=2.2
23
+ Requires-Dist: microsoft-kiota-http<1.13,>=1.11.6
23
24
  Requires-Dist: msal>=1.31
24
25
  Requires-Dist: msgraph-sdk>=1.0
25
26
  Requires-Dist: python-dotenv>=1.0
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "msgraph-mcp-server"
3
- version = "0.4.0"
3
+ version = "0.5.1"
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"
@@ -20,8 +20,15 @@ classifiers = [
20
20
  "Topic :: Office/Business :: Scheduling",
21
21
  ]
22
22
  dependencies = [
23
- "mcp>=1.0",
23
+ "mcp>=2.2,<3",
24
24
  "msgraph-sdk>=1.0",
25
+ # Transitive, bounded on purpose. kiota-http 1.13 moved request options
26
+ # from a request attribute into request.extensions, but msgraph-core (up to
27
+ # 1.5.1) only runs its middleware when that attribute exists. With 1.13+ the
28
+ # whole pipeline is skipped, so /me is never rewritten and every call fails
29
+ # with ErrorInvalidUser 'me-token-to-replace'. Drop this once msgraph-core
30
+ # reads the options from extensions.
31
+ "microsoft-kiota-http>=1.11.6,<1.13",
25
32
  "msal>=1.31",
26
33
  "python-dotenv>=1.0",
27
34
  ]
@@ -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.1",
11
11
  "packages": [
12
12
  {
13
13
  "registryType": "pypi",
14
14
  "identifier": "msgraph-mcp-server",
15
- "version": "0.4.0",
15
+ "version": "0.5.1",
16
16
  "runtimeHint": "uvx",
17
17
  "transport": {
18
18
  "type": "stdio"
@@ -12,7 +12,6 @@ from pathlib import Path
12
12
 
13
13
  from dotenv import find_dotenv, load_dotenv
14
14
 
15
-
16
15
  # Load .env once at module import; process env takes precedence.
17
16
  #
18
17
  # The working-directory search runs first because that is where a user expects
@@ -15,7 +15,6 @@ from azure.core.credentials import AccessToken, TokenCredential
15
15
 
16
16
  from msgraph_mcp.auth.token import get_access_token
17
17
 
18
-
19
18
  # Short expiry: MSAL's cache handles real refresh. Each get_token call goes
20
19
  # through get_access_token() which calls acquire_token_silent (cheap, cached).
21
20
  _BUFFER_SECONDS = 300
@@ -200,7 +200,9 @@ class GraphBatchTransport:
200
200
  import json
201
201
 
202
202
  from kiota_abstractions.method import Method # type: ignore[import-untyped]
203
- from kiota_abstractions.request_information import RequestInformation # type: ignore[import-untyped]
203
+ from kiota_abstractions.request_information import (
204
+ RequestInformation, # type: ignore[import-untyped]
205
+ )
204
206
 
205
207
  ri = RequestInformation()
206
208
  ri.url = self.GRAPH_BATCH_URL
@@ -12,7 +12,6 @@ from urllib.parse import urlparse
12
12
 
13
13
  from msgraph_mcp.graph.errors import GraphValidationError
14
14
 
15
-
16
15
  _ALLOWED_HOSTS = {"graph.microsoft.com"}
17
16
 
18
17
 
@@ -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
  }
@@ -1,20 +1,19 @@
1
- """FastMCP server entry point.
1
+ """MCP server entry point.
2
2
 
3
3
  Boot order:
4
4
  1. Validate config (fail fast if env is missing).
5
5
  2. Build the GraphClient (lazy token acquisition — no Graph calls yet).
6
- 3. Build a FastMCP app, register all tools, and run on stdio.
6
+ 3. Build an MCPServer app, register all tools, and run on stdio.
7
7
  """
8
8
 
9
9
  from __future__ import annotations
10
10
 
11
- from mcp.server.fastmcp import FastMCP
11
+ from mcp.server.mcpserver import MCPServer
12
12
 
13
13
  from msgraph_mcp import config
14
14
  from msgraph_mcp.graph.client import GraphClient
15
15
  from msgraph_mcp.tools import register_all
16
16
 
17
-
18
17
  _INSTRUCTIONS = """
19
18
  Microsoft Outlook (mail + calendar) and Microsoft Teams (read-only) via Microsoft Graph.
20
19
 
@@ -45,12 +44,12 @@ that returns objects to also get the full Graph payload.
45
44
  """.strip()
46
45
 
47
46
 
48
- def build_server() -> FastMCP:
47
+ def build_server() -> MCPServer:
49
48
  # Fail fast on missing env (raises ConfigError before we hit the event loop)
50
49
  config.client_id()
51
50
  config.authority()
52
51
 
53
- mcp = FastMCP(name="msgraph-mcp", instructions=_INSTRUCTIONS)
52
+ mcp = MCPServer(name="msgraph-mcp", instructions=_INSTRUCTIONS)
54
53
  graph = GraphClient()
55
54
  register_all(mcp, graph=graph)
56
55
  return mcp
@@ -1,6 +1,6 @@
1
1
  """Tool registration entry point.
2
2
 
3
- register_all(mcp) wires every tool into the FastMCP app. Each tool module
3
+ register_all(mcp) wires every tool into the MCPServer app. Each tool module
4
4
  exposes a register(mcp, *, graph) function so we can keep modules small.
5
5
  """
6
6
 
@@ -14,8 +14,8 @@ from msgraph_mcp.tools import (
14
14
  mail_read,
15
15
  mail_rules,
16
16
  mail_write,
17
- teams_chats,
18
17
  teams_channels,
18
+ teams_chats,
19
19
  teams_content,
20
20
  util,
21
21
  )
@@ -1,7 +1,7 @@
1
1
  """Shared helpers for tools that return downloaded binary content.
2
2
 
3
3
  Image bytes are returned to the client as a native MCP image block
4
- (mcp.server.fastmcp.utilities.types.Image) so agents see the image directly
4
+ (mcp.server.mcpserver.Image) so agents see the image directly
5
5
  instead of a base64 text payload; save_path writes bytes to disk instead and
6
6
  returns only metadata. Non-image content without save_path falls back to the
7
7
  legacy base64 dict.
@@ -12,7 +12,7 @@ from __future__ import annotations
12
12
  import pathlib
13
13
  from typing import TypeGuard
14
14
 
15
- from mcp.server.fastmcp.utilities.types import Image
15
+ from mcp.server.mcpserver import Image
16
16
 
17
17
  _EXT_BY_CONTENT_TYPE = {
18
18
  "image/png": ".png",
@@ -31,7 +31,7 @@ def ext_for(content_type: str | None) -> str:
31
31
 
32
32
 
33
33
  def image_result(meta: dict, data: bytes, content_type: str) -> list:
34
- """Metadata dict + native MCP image block (FastMCP renders both)."""
34
+ """Metadata dict + native MCP image block (MCPServer renders both)."""
35
35
  return [meta, Image(data=data, format=content_type.removeprefix("image/"))]
36
36
 
37
37
 
@@ -47,7 +47,6 @@ from msgraph_mcp.graph.pagination import (
47
47
  from msgraph_mcp.graph.serialize import calendar_to_dict, event_to_dict
48
48
  from msgraph_mcp.graph.trimming import trim_calendar, trim_event
49
49
 
50
-
51
50
  _VALID_RESPONSES = {
52
51
  "accept": "accepted",
53
52
  "tentativelyAccept": "tentativelyAccepted",
@@ -73,7 +72,7 @@ async def list_calendars(
73
72
  collection = await graph.mailbox(mailbox).calendars.get()
74
73
  except NotAuthenticatedError:
75
74
  raise
76
- except Exception as exc: # noqa: BLE001
75
+ except Exception as exc:
77
76
  raise map_kiota_error(exc) from exc
78
77
  items = [
79
78
  trim_calendar(calendar_to_dict(c), include_raw=include_raw)
@@ -139,7 +138,7 @@ async def list_events(
139
138
  )
140
139
  except NotAuthenticatedError:
141
140
  raise
142
- except Exception as exc: # noqa: BLE001
141
+ except Exception as exc:
143
142
  raise map_kiota_error(exc) from exc
144
143
 
145
144
  items = [
@@ -167,7 +166,7 @@ async def get_event(
167
166
  evt = await graph.mailbox(mailbox).events.by_event_id(event_id).get()
168
167
  except NotAuthenticatedError:
169
168
  raise
170
- except Exception as exc: # noqa: BLE001
169
+ except Exception as exc:
171
170
  raise map_kiota_error(exc) from exc
172
171
  return trim_event(
173
172
  event_to_dict(evt), include_body=include_body, include_raw=include_raw
@@ -187,6 +186,28 @@ def _free_busy(show_as: str) -> FreeBusyStatus:
187
186
  return _SHOW_AS[show_as]
188
187
 
189
188
 
189
+ def _apply_reminder(
190
+ evt: Event,
191
+ *,
192
+ is_reminder_on: bool | None,
193
+ reminder_minutes_before_start: int | None,
194
+ ) -> None:
195
+ """Set reminder fields on evt. Giving minutes turns the reminder on."""
196
+ if reminder_minutes_before_start is not None:
197
+ if reminder_minutes_before_start < 0:
198
+ raise GraphValidationError(
199
+ "reminder_minutes_before_start must be zero or greater"
200
+ )
201
+ if is_reminder_on is False:
202
+ raise GraphValidationError(
203
+ "reminder_minutes_before_start cannot be set when is_reminder_on is False"
204
+ )
205
+ evt.reminder_minutes_before_start = reminder_minutes_before_start
206
+ evt.is_reminder_on = True
207
+ elif is_reminder_on is not None:
208
+ evt.is_reminder_on = is_reminder_on
209
+
210
+
190
211
  def _build_event(
191
212
  *,
192
213
  subject: str,
@@ -200,6 +221,8 @@ def _build_event(
200
221
  is_online_meeting: bool,
201
222
  is_all_day: bool,
202
223
  show_as: ShowAs | None,
224
+ is_reminder_on: bool | None,
225
+ reminder_minutes_before_start: int | None,
203
226
  ) -> Event:
204
227
  e = Event()
205
228
  e.subject = subject
@@ -228,6 +251,11 @@ def _build_event(
228
251
  e.is_all_day = is_all_day
229
252
  if show_as is not None:
230
253
  e.show_as = _free_busy(show_as)
254
+ _apply_reminder(
255
+ e,
256
+ is_reminder_on=is_reminder_on,
257
+ reminder_minutes_before_start=reminder_minutes_before_start,
258
+ )
231
259
  return e
232
260
 
233
261
 
@@ -245,6 +273,8 @@ async def create_event(
245
273
  is_online_meeting: bool = False,
246
274
  is_all_day: bool = False,
247
275
  show_as: ShowAs | None = None,
276
+ is_reminder_on: bool | None = None,
277
+ reminder_minutes_before_start: int | None = None,
248
278
  calendar_id: str | None = None,
249
279
  mailbox: str | None = None,
250
280
  include_raw: bool = False,
@@ -263,6 +293,10 @@ async def create_event(
263
293
  is_all_day: All-day event flag.
264
294
  show_as: Free/busy status shown to others: "free", "tentative", "busy",
265
295
  "oof", or "workingElsewhere". Omit for Outlook's default ("busy").
296
+ is_reminder_on: False creates the event with no reminder. Omit for
297
+ Outlook's default (a reminder 15 minutes before start).
298
+ reminder_minutes_before_start: Minutes before start to alert (0 or
299
+ more). Setting this turns the reminder on.
266
300
  calendar_id: Optional calendar id; default is the user's primary calendar.
267
301
  mailbox: Optional mailbox.
268
302
 
@@ -281,6 +315,8 @@ async def create_event(
281
315
  is_online_meeting=is_online_meeting,
282
316
  is_all_day=is_all_day,
283
317
  show_as=show_as,
318
+ is_reminder_on=is_reminder_on,
319
+ reminder_minutes_before_start=reminder_minutes_before_start,
284
320
  )
285
321
  try:
286
322
  target = graph.mailbox(mailbox)
@@ -289,7 +325,7 @@ async def create_event(
289
325
  created = await target.events.post(evt)
290
326
  except NotAuthenticatedError:
291
327
  raise
292
- except Exception as exc: # noqa: BLE001
328
+ except Exception as exc:
293
329
  raise map_kiota_error(exc) from exc
294
330
  return trim_event(
295
331
  event_to_dict(created), include_body=False, include_raw=include_raw
@@ -310,6 +346,8 @@ async def update_event(
310
346
  is_online_meeting: bool | None = None,
311
347
  is_all_day: bool | None = None,
312
348
  show_as: ShowAs | None = None,
349
+ is_reminder_on: bool | None = None,
350
+ reminder_minutes_before_start: int | None = None,
313
351
  mailbox: str | None = None,
314
352
  include_raw: bool = False,
315
353
  ) -> dict:
@@ -319,6 +357,10 @@ async def update_event(
319
357
 
320
358
  show_as sets the free/busy status shown to others: "free", "tentative",
321
359
  "busy", "oof", or "workingElsewhere".
360
+
361
+ is_reminder_on=False removes the reminder. reminder_minutes_before_start
362
+ sets how many minutes before start to alert (0 or more) and turns the
363
+ reminder on.
322
364
  """
323
365
  if (start_datetime or end_datetime) and not time_zone:
324
366
  raise GraphValidationError("time_zone is required when updating start or end")
@@ -344,11 +386,16 @@ async def update_event(
344
386
  patch.is_all_day = is_all_day
345
387
  if show_as is not None:
346
388
  patch.show_as = _free_busy(show_as)
389
+ _apply_reminder(
390
+ patch,
391
+ is_reminder_on=is_reminder_on,
392
+ reminder_minutes_before_start=reminder_minutes_before_start,
393
+ )
347
394
  try:
348
395
  updated = await graph.mailbox(mailbox).events.by_event_id(event_id).patch(patch)
349
396
  except NotAuthenticatedError:
350
397
  raise
351
- except Exception as exc: # noqa: BLE001
398
+ except Exception as exc:
352
399
  raise map_kiota_error(exc) from exc
353
400
  return trim_event(
354
401
  event_to_dict(updated), include_body=False, include_raw=include_raw
@@ -361,7 +408,7 @@ async def delete_event(*, graph, event_id: str, mailbox: str | None = None) -> d
361
408
  await graph.mailbox(mailbox).events.by_event_id(event_id).delete()
362
409
  except NotAuthenticatedError:
363
410
  raise
364
- except Exception as exc: # noqa: BLE001
411
+ except Exception as exc:
365
412
  raise map_kiota_error(exc) from exc
366
413
  return {"status": "deleted"}
367
414
 
@@ -377,7 +424,7 @@ async def cancel_event(
377
424
  await graph.mailbox(mailbox).events.by_event_id(event_id).cancel.post(body)
378
425
  except NotAuthenticatedError:
379
426
  raise
380
- except Exception as exc: # noqa: BLE001
427
+ except Exception as exc:
381
428
  raise map_kiota_error(exc) from exc
382
429
  return {"status": "cancelled"}
383
430
 
@@ -422,7 +469,7 @@ async def respond_to_event(
422
469
  await evt_builder.decline.post(decline_body)
423
470
  except NotAuthenticatedError:
424
471
  raise
425
- except Exception as exc: # noqa: BLE001
472
+ except Exception as exc:
426
473
  raise map_kiota_error(exc) from exc
427
474
  return {"status": _VALID_RESPONSES[response]}
428
475
 
@@ -488,7 +535,7 @@ async def find_meeting_times(
488
535
  result = await graph.mailbox(mailbox).find_meeting_times.post(body)
489
536
  except NotAuthenticatedError:
490
537
  raise
491
- except Exception as exc: # noqa: BLE001
538
+ except Exception as exc:
492
539
  raise map_kiota_error(exc) from exc
493
540
 
494
541
  suggestions_raw = getattr(result, "meeting_time_suggestions", None) or []
@@ -578,6 +625,8 @@ def register(mcp, *, graph) -> None:
578
625
  is_online_meeting: bool = False,
579
626
  is_all_day: bool = False,
580
627
  show_as: ShowAs | None = None,
628
+ is_reminder_on: bool | None = None,
629
+ reminder_minutes_before_start: int | None = None,
581
630
  calendar_id: str | None = None,
582
631
  mailbox: str | None = None,
583
632
  include_raw: bool = False,
@@ -595,6 +644,8 @@ def register(mcp, *, graph) -> None:
595
644
  is_online_meeting=is_online_meeting,
596
645
  is_all_day=is_all_day,
597
646
  show_as=show_as,
647
+ is_reminder_on=is_reminder_on,
648
+ reminder_minutes_before_start=reminder_minutes_before_start,
598
649
  calendar_id=calendar_id,
599
650
  mailbox=mailbox,
600
651
  include_raw=include_raw,
@@ -613,6 +664,8 @@ def register(mcp, *, graph) -> None:
613
664
  is_online_meeting: bool | None = None,
614
665
  is_all_day: bool | None = None,
615
666
  show_as: ShowAs | None = None,
667
+ is_reminder_on: bool | None = None,
668
+ reminder_minutes_before_start: int | None = None,
616
669
  mailbox: str | None = None,
617
670
  include_raw: bool = False,
618
671
  ):
@@ -629,6 +682,8 @@ def register(mcp, *, graph) -> None:
629
682
  is_online_meeting=is_online_meeting,
630
683
  is_all_day=is_all_day,
631
684
  show_as=show_as,
685
+ is_reminder_on=is_reminder_on,
686
+ reminder_minutes_before_start=reminder_minutes_before_start,
632
687
  mailbox=mailbox,
633
688
  include_raw=include_raw,
634
689
  )
@@ -31,7 +31,7 @@ async def list_folders(
31
31
  collection = await graph.mailbox(mailbox).mail_folders.get()
32
32
  except NotAuthenticatedError:
33
33
  raise
34
- except Exception as exc: # noqa: BLE001
34
+ except Exception as exc:
35
35
  raise map_kiota_error(exc) from exc
36
36
  items = [
37
37
  trim_folder(folder_to_dict(f), include_raw=include_raw)
@@ -70,7 +70,7 @@ async def move_message(
70
70
  )
71
71
  except NotAuthenticatedError:
72
72
  raise
73
- except Exception as exc: # noqa: BLE001
73
+ except Exception as exc:
74
74
  raise map_kiota_error(exc) from exc
75
75
  return trim_message(
76
76
  message_to_dict(moved), include_body=False, include_raw=include_raw
@@ -113,7 +113,7 @@ async def create_folder(
113
113
  created = await graph.mailbox(mailbox).mail_folders.post(body)
114
114
  except NotAuthenticatedError:
115
115
  raise
116
- except Exception as exc: # noqa: BLE001
116
+ except Exception as exc:
117
117
  raise map_kiota_error(exc) from exc
118
118
  return trim_folder(folder_to_dict(created), include_raw=include_raw)
119
119
 
@@ -166,7 +166,7 @@ async def update_folder(
166
166
  result = await folder_endpoint.move.post(move_body)
167
167
  except NotAuthenticatedError:
168
168
  raise
169
- except Exception as exc: # noqa: BLE001
169
+ except Exception as exc:
170
170
  raise map_kiota_error(exc) from exc
171
171
  return trim_folder(folder_to_dict(result), include_raw=include_raw)
172
172
 
@@ -198,7 +198,7 @@ async def delete_folder(
198
198
  folder = await folder_endpoint.get()
199
199
  except NotAuthenticatedError:
200
200
  raise
201
- except Exception as exc: # noqa: BLE001
201
+ except Exception as exc:
202
202
  raise map_kiota_error(exc) from exc
203
203
  total = getattr(folder, "total_item_count", None) or 0
204
204
  children = getattr(folder, "child_folder_count", None) or 0
@@ -212,7 +212,7 @@ async def delete_folder(
212
212
  await folder_endpoint.delete()
213
213
  except NotAuthenticatedError:
214
214
  raise
215
- except Exception as exc: # noqa: BLE001
215
+ except Exception as exc:
216
216
  raise map_kiota_error(exc) from exc
217
217
  return {"deleted": True, "id": folder_id}
218
218
 
@@ -109,7 +109,7 @@ async def list_messages(
109
109
  )
110
110
  except NotAuthenticatedError:
111
111
  raise
112
- except Exception as exc: # noqa: BLE001
112
+ except Exception as exc:
113
113
  raise map_kiota_error(exc) from exc
114
114
 
115
115
  items = [
@@ -166,7 +166,7 @@ async def search_messages(
166
166
  )
167
167
  except NotAuthenticatedError:
168
168
  raise
169
- except Exception as exc: # noqa: BLE001
169
+ except Exception as exc:
170
170
  raise map_kiota_error(exc) from exc
171
171
 
172
172
  items = [
@@ -205,7 +205,7 @@ async def get_message(
205
205
  msg = await graph.mailbox(mailbox).messages.by_message_id(message_id).get()
206
206
  except NotAuthenticatedError:
207
207
  raise
208
- except Exception as exc: # noqa: BLE001
208
+ except Exception as exc:
209
209
  raise map_kiota_error(exc) from exc
210
210
  return trim_message(
211
211
  message_to_dict(msg), include_body=include_body, include_raw=include_raw
@@ -228,7 +228,7 @@ async def list_attachments(
228
228
  )
229
229
  except NotAuthenticatedError:
230
230
  raise
231
- except Exception as exc: # noqa: BLE001
231
+ except Exception as exc:
232
232
  raise map_kiota_error(exc) from exc
233
233
  items = [
234
234
  trim_attachment_list(
@@ -270,7 +270,7 @@ async def download_attachment(
270
270
  )
271
271
  except NotAuthenticatedError:
272
272
  raise
273
- except Exception as exc: # noqa: BLE001
273
+ except Exception as exc:
274
274
  raise map_kiota_error(exc) from exc
275
275
 
276
276
  content = getattr(att, "content_bytes", None)
@@ -35,7 +35,7 @@ async def list_rules(
35
35
  collection = await _rules_endpoint(graph, mailbox).get()
36
36
  except NotAuthenticatedError:
37
37
  raise
38
- except Exception as exc: # noqa: BLE001
38
+ except Exception as exc:
39
39
  raise map_kiota_error(exc) from exc
40
40
  items = [
41
41
  trim_message_rule(message_rule_to_dict(r), include_raw=include_raw)
@@ -63,7 +63,7 @@ async def get_rule(
63
63
  rule = await _rules_endpoint(graph, mailbox).by_message_rule_id(rule_id).get()
64
64
  except NotAuthenticatedError:
65
65
  raise
66
- except Exception as exc: # noqa: BLE001
66
+ except Exception as exc:
67
67
  raise map_kiota_error(exc) from exc
68
68
  return trim_message_rule(message_rule_to_dict(rule), include_raw=include_raw)
69
69
 
@@ -241,7 +241,7 @@ async def create_rule(
241
241
  created = await _rules_endpoint(graph, mailbox).post(body)
242
242
  except NotAuthenticatedError:
243
243
  raise
244
- except Exception as exc: # noqa: BLE001
244
+ except Exception as exc:
245
245
  raise map_kiota_error(exc) from exc
246
246
  return trim_message_rule(message_rule_to_dict(created), include_raw=include_raw)
247
247
 
@@ -346,7 +346,7 @@ async def update_rule(
346
346
  )
347
347
  except NotAuthenticatedError:
348
348
  raise
349
- except Exception as exc: # noqa: BLE001
349
+ except Exception as exc:
350
350
  raise map_kiota_error(exc) from exc
351
351
  return trim_message_rule(message_rule_to_dict(updated), include_raw=include_raw)
352
352
 
@@ -367,7 +367,7 @@ async def delete_rule(*, graph, rule_id: str, mailbox: str | None = None) -> dic
367
367
  await _rules_endpoint(graph, mailbox).by_message_rule_id(rule_id).delete()
368
368
  except NotAuthenticatedError:
369
369
  raise
370
- except Exception as exc: # noqa: BLE001
370
+ except Exception as exc:
371
371
  raise map_kiota_error(exc) from exc
372
372
  return {"deleted": True, "id": rule_id}
373
373
 
@@ -33,7 +33,6 @@ from msgraph_mcp.graph.errors import GraphValidationError, map_kiota_error
33
33
  from msgraph_mcp.graph.serialize import message_to_dict
34
34
  from msgraph_mcp.graph.trimming import trim_message
35
35
 
36
-
37
36
  _MAX_INLINE_ATTACHMENT_BYTES = 3 * 1024 * 1024
38
37
 
39
38
 
@@ -153,7 +152,7 @@ async def send_message(
153
152
  await graph.mailbox(mailbox).send_mail.post(req)
154
153
  except NotAuthenticatedError:
155
154
  raise
156
- except Exception as exc: # noqa: BLE001
155
+ except Exception as exc:
157
156
  raise map_kiota_error(exc) from exc
158
157
  return {"status": "sent"}
159
158
 
@@ -185,7 +184,7 @@ async def create_draft(
185
184
  created = await graph.mailbox(mailbox).messages.post(msg)
186
185
  except NotAuthenticatedError:
187
186
  raise
188
- except Exception as exc: # noqa: BLE001
187
+ except Exception as exc:
189
188
  raise map_kiota_error(exc) from exc
190
189
  return trim_message(
191
190
  message_to_dict(created), include_body=False, include_raw=include_raw
@@ -235,7 +234,7 @@ async def reply_message(
235
234
  )
236
235
  except NotAuthenticatedError:
237
236
  raise
238
- except Exception as exc: # noqa: BLE001
237
+ except Exception as exc:
239
238
  raise map_kiota_error(exc) from exc
240
239
  return {"status": "sent"}
241
240
 
@@ -256,7 +255,7 @@ async def reply_all_message(
256
255
  )
257
256
  except NotAuthenticatedError:
258
257
  raise
259
- except Exception as exc: # noqa: BLE001
258
+ except Exception as exc:
260
259
  raise map_kiota_error(exc) from exc
261
260
  return {"status": "sent"}
262
261
 
@@ -280,7 +279,7 @@ async def forward_message(
280
279
  )
281
280
  except NotAuthenticatedError:
282
281
  raise
283
- except Exception as exc: # noqa: BLE001
282
+ except Exception as exc:
284
283
  raise map_kiota_error(exc) from exc
285
284
  return {"status": "sent"}
286
285
 
@@ -343,7 +342,7 @@ async def update_message(
343
342
  )
344
343
  except NotAuthenticatedError:
345
344
  raise
346
- except Exception as exc: # noqa: BLE001
345
+ except Exception as exc:
347
346
  raise map_kiota_error(exc) from exc
348
347
  return trim_message(
349
348
  message_to_dict(updated), include_body=False, include_raw=include_raw
@@ -356,7 +355,7 @@ async def delete_message(*, graph, message_id: str, mailbox: str | None = None)
356
355
  await graph.mailbox(mailbox).messages.by_message_id(message_id).delete()
357
356
  except NotAuthenticatedError:
358
357
  raise
359
- except Exception as exc: # noqa: BLE001
358
+ except Exception as exc:
360
359
  raise map_kiota_error(exc) from exc
361
360
  return {"status": "deleted"}
362
361