matimo-microsoft 0.1.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 (26) hide show
  1. matimo_microsoft-0.1.0/.gitignore +63 -0
  2. matimo_microsoft-0.1.0/PKG-INFO +119 -0
  3. matimo_microsoft-0.1.0/README.md +105 -0
  4. matimo_microsoft-0.1.0/pyproject.toml +27 -0
  5. matimo_microsoft-0.1.0/src/matimo_microsoft/__init__.py +17 -0
  6. matimo_microsoft-0.1.0/src/matimo_microsoft/graph_client.py +236 -0
  7. matimo_microsoft-0.1.0/src/matimo_microsoft/tools/ms_create_calendar_event/definition.yaml +100 -0
  8. matimo_microsoft-0.1.0/src/matimo_microsoft/tools/ms_create_calendar_event/ms_create_calendar_event.py +81 -0
  9. matimo_microsoft-0.1.0/src/matimo_microsoft/tools/ms_create_document/definition.yaml +103 -0
  10. matimo_microsoft-0.1.0/src/matimo_microsoft/tools/ms_create_document/ms_create_document.py +106 -0
  11. matimo_microsoft-0.1.0/src/matimo_microsoft/tools/ms_get_email/definition.yaml +88 -0
  12. matimo_microsoft-0.1.0/src/matimo_microsoft/tools/ms_get_email/ms_get_email.py +94 -0
  13. matimo_microsoft-0.1.0/src/matimo_microsoft/tools/ms_list_files/definition.yaml +81 -0
  14. matimo_microsoft-0.1.0/src/matimo_microsoft/tools/ms_list_files/ms_list_files.py +72 -0
  15. matimo_microsoft-0.1.0/src/matimo_microsoft/tools/ms_publish_to_sharepoint/definition.yaml +92 -0
  16. matimo_microsoft-0.1.0/src/matimo_microsoft/tools/ms_publish_to_sharepoint/ms_publish_to_sharepoint.py +122 -0
  17. matimo_microsoft-0.1.0/src/matimo_microsoft/tools/ms_read_file/definition.yaml +74 -0
  18. matimo_microsoft-0.1.0/src/matimo_microsoft/tools/ms_read_file/ms_read_file.py +96 -0
  19. matimo_microsoft-0.1.0/src/matimo_microsoft/tools/ms_search_knowledge/definition.yaml +99 -0
  20. matimo_microsoft-0.1.0/src/matimo_microsoft/tools/ms_search_knowledge/ms_search_knowledge.py +109 -0
  21. matimo_microsoft-0.1.0/src/matimo_microsoft/tools/ms_send_email/definition.yaml +94 -0
  22. matimo_microsoft-0.1.0/src/matimo_microsoft/tools/ms_send_email/ms_send_email.py +99 -0
  23. matimo_microsoft-0.1.0/src/matimo_microsoft/tools/ms_send_teams_message/definition.yaml +87 -0
  24. matimo_microsoft-0.1.0/src/matimo_microsoft/tools/ms_send_teams_message/ms_send_teams_message.py +59 -0
  25. matimo_microsoft-0.1.0/tests/unit/test_graph_client.py +328 -0
  26. matimo_microsoft-0.1.0/tests/unit/test_microsoft_tools.py +952 -0
@@ -0,0 +1,63 @@
1
+ # Dependencies
2
+ **/node_modules/
3
+ package-lock.json
4
+ yarn.lock
5
+
6
+ # Build output
7
+ **/dist/
8
+ *.tsbuildinfo
9
+
10
+ # Python compiled / build
11
+ **/__pycache__/
12
+ *.py[cod]
13
+ *$py.class
14
+ *.egg-info/
15
+ *.egg
16
+ **/build/
17
+ **/.eggs/
18
+ **/.venv/
19
+ **/.mypy_cache/
20
+ **/.ruff_cache/
21
+ **/.pytest_cache/
22
+
23
+ # Test coverage
24
+ **/coverage/
25
+ **/.nyc_output/
26
+
27
+ # IDE
28
+ .vscode/
29
+ .idea/
30
+ *.swp
31
+ *.swo
32
+ *~
33
+ .DS_Store
34
+
35
+ # Environment
36
+ .env
37
+ .env.local
38
+ .env.*.local
39
+
40
+ # Logs
41
+ *.log
42
+ npm-debug.log*
43
+ yarn-debug.log*
44
+ yarn-error.log*
45
+
46
+ # OS
47
+ .DS_Store
48
+ Thumbs.db
49
+
50
+ # Temporary files
51
+ tmp/
52
+ temp/
53
+ *.tmp
54
+ typescript/examples/mcp/matimo-tools/.matimo-approvals.json
55
+ typescript/examples/mcp/matimo-tools/fetch-weather/definition.yaml
56
+ typescript/examples/mcp/matimo-tools/npm_downloads/definition.yaml
57
+ typescript/examples/mcp/matimo-tools/skills/ecosystem-health/SKILL.md
58
+ typescript/examples/mcp/matimo-tools/skills/matimo-health-check/SKILL.md
59
+ typescript/examples/mcp/matimo-tools/skills/moltbook-identity/SKILL.md
60
+ typescript/packages/cli/.matimo/certs/server.crt
61
+ typescript/packages/cli/.matimo/certs/server.key
62
+ typescript/examples/mcp/.matimo/certs/server.crt
63
+ typescript/examples/mcp/.matimo/certs/server.key
@@ -0,0 +1,119 @@
1
+ Metadata-Version: 2.4
2
+ Name: matimo-microsoft
3
+ Version: 0.1.0
4
+ Summary: Matimo provider — Microsoft Graph tools (mail, calendar, Teams, files, SharePoint)
5
+ License: MIT
6
+ Keywords: agents,ai,graph,matimo,microsoft,outlook,sharepoint,teams,tools
7
+ Classifier: Development Status :: 4 - Beta
8
+ Classifier: Intended Audience :: Developers
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Programming Language :: Python :: 3.11
11
+ Requires-Python: >=3.11
12
+ Requires-Dist: matimo-core<0.2.0,>=0.1.0
13
+ Description-Content-Type: text/markdown
14
+
15
+ # matimo-microsoft
16
+
17
+ > Microsoft Graph tools for [Matimo](https://matimo.dev) — search, OneDrive/SharePoint
18
+ > files, Outlook mail, Microsoft Teams, calendar, and SharePoint publishing.
19
+
20
+ [![PyPI](https://img.shields.io/pypi/v/matimo-microsoft)](https://pypi.org/project/matimo-microsoft/)
21
+ [![Docs](https://img.shields.io/badge/docs-matimo.dev-blue)](https://matimo.dev/docs)
22
+
23
+ ---
24
+
25
+ ## Installation
26
+
27
+ ```bash
28
+ pip install matimo matimo-microsoft
29
+ ```
30
+
31
+ ---
32
+
33
+ ## Available Tools (9 Total)
34
+
35
+ | Tool | Description | Risk | Graph endpoint |
36
+ |------|-------------|------|----------------|
37
+ | `ms_search_knowledge` | Search SharePoint sites, OneDrive/SharePoint files, and list items | low | `POST /search/query` |
38
+ | `ms_read_file` | Read a OneDrive/SharePoint file's contents (plain-text formats only) | low | `GET /drives/{id}/items/{id}/content` |
39
+ | `ms_list_files` | List the children of a OneDrive/SharePoint folder | low | `GET /drives/{id}/items/{id}/children` |
40
+ | `ms_get_email` | List messages in the signed-in user's mailbox | low | `GET /me/messages` |
41
+ | `ms_send_email` | Send an email as the signed-in user | **high** (approval) | `POST /me/messages` + `/send` |
42
+ | `ms_send_teams_message` | Post (or reply to) a message in a Teams channel | medium | `POST /teams/{id}/channels/{id}/messages` |
43
+ | `ms_create_document` | Upload a small file to OneDrive/SharePoint (≤4 MB) | medium | `PUT /drives/{id}/items/{id}:/{name}:/content` |
44
+ | `ms_create_calendar_event` | Create a calendar event, optionally as a Teams meeting | medium | `POST /me/events` |
45
+ | `ms_publish_to_sharepoint` | Create and publish a SharePoint site page | **high** (approval) | `POST /sites/{id}/pages` + `/publish` |
46
+
47
+ ---
48
+
49
+ ## Quick Start
50
+
51
+ ```python
52
+ from matimo import MatimoInstance
53
+
54
+ matimo = await MatimoInstance.init(auto_discover=True)
55
+
56
+ # Search across SharePoint and OneDrive
57
+ search = await matimo.execute("ms_search_knowledge", {"query": "Q3 budget filetype:xlsx", "top": 5})
58
+
59
+ # List messages in the signed-in user's mailbox
60
+ inbox = await matimo.execute("ms_get_email", {"top": 5, "filter": "isRead eq false"})
61
+
62
+ # Send an email (requires_approval: true — routed through HITL)
63
+ await matimo.execute(
64
+ "ms_send_email",
65
+ {"to": ["alice@contoso.com"], "subject": "Weekly status update", "body": "Here is the summary..."},
66
+ )
67
+ ```
68
+
69
+ ---
70
+
71
+ ## Authentication
72
+
73
+ Microsoft Graph tools use delegated OAuth2 access tokens. Matimo never performs the
74
+ OAuth code exchange itself — connect Microsoft through your Matimo deployment (Nova),
75
+ then provide the resulting token at execution time:
76
+
77
+ ```bash
78
+ export MICROSOFT_GRAPH_ACCESS_TOKEN="eyJ0eXAiOiJKV1Qi..."
79
+ ```
80
+
81
+ or pass it through per-call credentials:
82
+
83
+ ```python
84
+ await matimo.execute(
85
+ "ms_get_email",
86
+ {"top": 5},
87
+ credentials={"MICROSOFT_GRAPH_ACCESS_TOKEN": token},
88
+ )
89
+ ```
90
+
91
+ ---
92
+
93
+ ## Risk & Approval
94
+
95
+ `ms_send_email` and `ms_publish_to_sharepoint` are marked `risk: high` and
96
+ `requires_approval: true` — Matimo routes them through the human-in-the-loop approval
97
+ flow before they execute, since they send mail and publish content visible to others
98
+ on the user's behalf. `ms_send_teams_message`, `ms_create_document`, and
99
+ `ms_create_calendar_event` are `risk: medium` (external writes, narrower blast radius).
100
+ The remaining read-only tools are `risk: low`.
101
+
102
+ ---
103
+
104
+ ## Documentation
105
+
106
+ - [Microsoft Graph API overview](https://learn.microsoft.com/en-us/graph/overview)
107
+ - [Graph Explorer](https://developer.microsoft.com/en-us/graph/graph-explorer)
108
+ - [Python Examples — Direct SDK](https://github.com/tallclub/matimo/tree/main/python/examples/native/microsoft)
109
+ - [Python Examples — LangChain agent](https://github.com/tallclub/matimo/tree/main/python/examples/langchain/microsoft)
110
+ - [Python Examples — CrewAI crew](https://github.com/tallclub/matimo/tree/main/python/examples/crewai/microsoft)
111
+
112
+ ---
113
+
114
+ ## Links
115
+
116
+ - **PyPI:** https://pypi.org/project/matimo-microsoft/
117
+ - **GitHub:** https://github.com/tallclub/matimo
118
+ - **Microsoft Graph API Docs:** https://learn.microsoft.com/en-us/graph/overview
119
+ - **Matimo documentation:** https://matimo.dev/docs
@@ -0,0 +1,105 @@
1
+ # matimo-microsoft
2
+
3
+ > Microsoft Graph tools for [Matimo](https://matimo.dev) — search, OneDrive/SharePoint
4
+ > files, Outlook mail, Microsoft Teams, calendar, and SharePoint publishing.
5
+
6
+ [![PyPI](https://img.shields.io/pypi/v/matimo-microsoft)](https://pypi.org/project/matimo-microsoft/)
7
+ [![Docs](https://img.shields.io/badge/docs-matimo.dev-blue)](https://matimo.dev/docs)
8
+
9
+ ---
10
+
11
+ ## Installation
12
+
13
+ ```bash
14
+ pip install matimo matimo-microsoft
15
+ ```
16
+
17
+ ---
18
+
19
+ ## Available Tools (9 Total)
20
+
21
+ | Tool | Description | Risk | Graph endpoint |
22
+ |------|-------------|------|----------------|
23
+ | `ms_search_knowledge` | Search SharePoint sites, OneDrive/SharePoint files, and list items | low | `POST /search/query` |
24
+ | `ms_read_file` | Read a OneDrive/SharePoint file's contents (plain-text formats only) | low | `GET /drives/{id}/items/{id}/content` |
25
+ | `ms_list_files` | List the children of a OneDrive/SharePoint folder | low | `GET /drives/{id}/items/{id}/children` |
26
+ | `ms_get_email` | List messages in the signed-in user's mailbox | low | `GET /me/messages` |
27
+ | `ms_send_email` | Send an email as the signed-in user | **high** (approval) | `POST /me/messages` + `/send` |
28
+ | `ms_send_teams_message` | Post (or reply to) a message in a Teams channel | medium | `POST /teams/{id}/channels/{id}/messages` |
29
+ | `ms_create_document` | Upload a small file to OneDrive/SharePoint (≤4 MB) | medium | `PUT /drives/{id}/items/{id}:/{name}:/content` |
30
+ | `ms_create_calendar_event` | Create a calendar event, optionally as a Teams meeting | medium | `POST /me/events` |
31
+ | `ms_publish_to_sharepoint` | Create and publish a SharePoint site page | **high** (approval) | `POST /sites/{id}/pages` + `/publish` |
32
+
33
+ ---
34
+
35
+ ## Quick Start
36
+
37
+ ```python
38
+ from matimo import MatimoInstance
39
+
40
+ matimo = await MatimoInstance.init(auto_discover=True)
41
+
42
+ # Search across SharePoint and OneDrive
43
+ search = await matimo.execute("ms_search_knowledge", {"query": "Q3 budget filetype:xlsx", "top": 5})
44
+
45
+ # List messages in the signed-in user's mailbox
46
+ inbox = await matimo.execute("ms_get_email", {"top": 5, "filter": "isRead eq false"})
47
+
48
+ # Send an email (requires_approval: true — routed through HITL)
49
+ await matimo.execute(
50
+ "ms_send_email",
51
+ {"to": ["alice@contoso.com"], "subject": "Weekly status update", "body": "Here is the summary..."},
52
+ )
53
+ ```
54
+
55
+ ---
56
+
57
+ ## Authentication
58
+
59
+ Microsoft Graph tools use delegated OAuth2 access tokens. Matimo never performs the
60
+ OAuth code exchange itself — connect Microsoft through your Matimo deployment (Nova),
61
+ then provide the resulting token at execution time:
62
+
63
+ ```bash
64
+ export MICROSOFT_GRAPH_ACCESS_TOKEN="eyJ0eXAiOiJKV1Qi..."
65
+ ```
66
+
67
+ or pass it through per-call credentials:
68
+
69
+ ```python
70
+ await matimo.execute(
71
+ "ms_get_email",
72
+ {"top": 5},
73
+ credentials={"MICROSOFT_GRAPH_ACCESS_TOKEN": token},
74
+ )
75
+ ```
76
+
77
+ ---
78
+
79
+ ## Risk & Approval
80
+
81
+ `ms_send_email` and `ms_publish_to_sharepoint` are marked `risk: high` and
82
+ `requires_approval: true` — Matimo routes them through the human-in-the-loop approval
83
+ flow before they execute, since they send mail and publish content visible to others
84
+ on the user's behalf. `ms_send_teams_message`, `ms_create_document`, and
85
+ `ms_create_calendar_event` are `risk: medium` (external writes, narrower blast radius).
86
+ The remaining read-only tools are `risk: low`.
87
+
88
+ ---
89
+
90
+ ## Documentation
91
+
92
+ - [Microsoft Graph API overview](https://learn.microsoft.com/en-us/graph/overview)
93
+ - [Graph Explorer](https://developer.microsoft.com/en-us/graph/graph-explorer)
94
+ - [Python Examples — Direct SDK](https://github.com/tallclub/matimo/tree/main/python/examples/native/microsoft)
95
+ - [Python Examples — LangChain agent](https://github.com/tallclub/matimo/tree/main/python/examples/langchain/microsoft)
96
+ - [Python Examples — CrewAI crew](https://github.com/tallclub/matimo/tree/main/python/examples/crewai/microsoft)
97
+
98
+ ---
99
+
100
+ ## Links
101
+
102
+ - **PyPI:** https://pypi.org/project/matimo-microsoft/
103
+ - **GitHub:** https://github.com/tallclub/matimo
104
+ - **Microsoft Graph API Docs:** https://learn.microsoft.com/en-us/graph/overview
105
+ - **Matimo documentation:** https://matimo.dev/docs
@@ -0,0 +1,27 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "matimo-microsoft"
7
+ version = "0.1.0"
8
+ description = "Matimo provider — Microsoft Graph tools (mail, calendar, Teams, files, SharePoint)"
9
+ readme = "README.md"
10
+ license = { text = "MIT" }
11
+ requires-python = ">=3.11"
12
+ keywords = ["ai", "tools", "agents", "matimo", "microsoft", "graph", "outlook", "teams", "sharepoint"]
13
+ classifiers = [
14
+ "Development Status :: 4 - Beta",
15
+ "Intended Audience :: Developers",
16
+ "License :: OSI Approved :: MIT License",
17
+ "Programming Language :: Python :: 3.11",
18
+ ]
19
+ dependencies = [
20
+ "matimo-core>=0.1.0,<0.2.0",
21
+ ]
22
+
23
+ [project.entry-points."matimo.providers"]
24
+ microsoft = "matimo_microsoft:get_tools_path"
25
+
26
+ [tool.hatch.build.targets.wheel]
27
+ packages = ["src/matimo_microsoft"]
@@ -0,0 +1,17 @@
1
+ """Matimo microsoft provider — exposes the path to YAML tool definitions."""
2
+ from __future__ import annotations
3
+
4
+ import importlib.resources
5
+ from pathlib import Path
6
+
7
+
8
+ def get_tools_path() -> str:
9
+ """Return the absolute path to the bundled microsoft tool definitions."""
10
+ try:
11
+ ref = importlib.resources.files("matimo_microsoft") / "tools"
12
+ return str(ref)
13
+ except Exception:
14
+ return str(Path(__file__).parent / "tools")
15
+
16
+
17
+ __all__ = ["get_tools_path"]
@@ -0,0 +1,236 @@
1
+ """
2
+ Shared Microsoft Graph helpers for all 'type: function' tools in this package.
3
+ Mirrors: typescript/packages/microsoft/tools/graph-client.ts
4
+
5
+ Conventions (mirrors matimo-slack and matimo-gmail):
6
+ - Tools NEVER perform OAuth token exchange. A delegated Graph access token is
7
+ injected at execution time via params['MICROSOFT_GRAPH_ACCESS_TOKEN'] (merged
8
+ from credentials by the function executor) or the MICROSOFT_GRAPH_ACCESS_TOKEN
9
+ environment variable as a fallback.
10
+ - Every Graph error is normalized into a MatimoError with the closest matching
11
+ ErrorCode (Matimo has no per-provider error classes — see matimo.errors).
12
+ - Unlike the TypeScript executor, Python's MatimoInstance.execute() re-raises
13
+ MatimoError rather than converting it to {success: False} — every failure path
14
+ here raises directly.
15
+ """
16
+ from __future__ import annotations
17
+
18
+ import asyncio
19
+ import os
20
+ from typing import Any, Literal
21
+
22
+ import httpx
23
+
24
+ from matimo.errors import ErrorCode, MatimoError
25
+
26
+ GRAPH_BASE_URL = "https://graph.microsoft.com/v1.0"
27
+
28
+ _RETRYABLE_STATUS_CODES = {429, 500, 503}
29
+ _MAX_RETRIES = 3
30
+ _INITIAL_BACKOFF_S = 0.5
31
+
32
+ HttpMethod = Literal["GET", "POST", "PUT", "PATCH", "DELETE"]
33
+ ResponseType = Literal["json", "bytes"]
34
+
35
+
36
+ def get_access_token(params: dict[str, Any]) -> str:
37
+ """
38
+ Resolve the delegated Graph access token. Matimo never exchanges OAuth codes —
39
+ the token must already be present in the merged params (credentials) or the
40
+ environment.
41
+ """
42
+ token = params.get("MICROSOFT_GRAPH_ACCESS_TOKEN") or os.environ.get(
43
+ "MICROSOFT_GRAPH_ACCESS_TOKEN"
44
+ )
45
+
46
+ if not token:
47
+ raise MatimoError(
48
+ "Microsoft Graph access token is missing. Provide it via "
49
+ "credentials.MICROSOFT_GRAPH_ACCESS_TOKEN or the MICROSOFT_GRAPH_ACCESS_TOKEN "
50
+ "environment variable. Matimo never performs the OAuth exchange itself — "
51
+ "connect Microsoft in Nova first.",
52
+ ErrorCode.AUTH_FAILED,
53
+ {"provider": "microsoft", "placeholder": "MICROSOFT_GRAPH_ACCESS_TOKEN"},
54
+ )
55
+
56
+ return str(token)
57
+
58
+
59
+ def require_params(params: dict[str, Any], required: list[str], tool_name: str) -> None:
60
+ """
61
+ Validate required parameters BEFORE any network call, mirroring the
62
+ "ValidationError before any API call" requirement. Raises VALIDATION_FAILED.
63
+ """
64
+ missing = [name for name in required if params.get(name) in (None, "")]
65
+
66
+ if missing:
67
+ raise MatimoError(
68
+ f"{tool_name}: missing required parameter(s): {', '.join(missing)}",
69
+ ErrorCode.VALIDATION_FAILED,
70
+ {"toolName": tool_name, "missingParams": missing},
71
+ )
72
+
73
+
74
+ def _parse_retry_after_seconds(retry_after_header: str | None) -> float | None:
75
+ """
76
+ Parse a `Retry-After` header into delta-seconds, returning None when it's
77
+ absent or not a numeric delay. Per RFC 9110 §10.2.3, `Retry-After` MAY be
78
+ an HTTP-date instead of delta-seconds — float() raises ValueError on that
79
+ (unlike JS's Number(), which yields NaN), so the conversion must be guarded
80
+ to avoid crashing error mapping for 429 responses.
81
+ """
82
+ if retry_after_header is None:
83
+ return None
84
+ try:
85
+ return float(retry_after_header)
86
+ except (TypeError, ValueError):
87
+ return None
88
+
89
+
90
+ def map_graph_error(
91
+ status: int,
92
+ data: Any,
93
+ headers: httpx.Headers | dict[str, str] | None,
94
+ resource_type: str,
95
+ ) -> MatimoError:
96
+ """
97
+ Map a Microsoft Graph HTTP error response onto a MatimoError using the closest
98
+ matching ErrorCode (Matimo has no CredentialError/NotFoundError/ProviderError
99
+ classes — see matimo.errors):
100
+ 401/403 -> AUTH_FAILED ("Microsoft Graph access denied. Check connection status in Nova.")
101
+ 404 -> FILE_NOT_FOUND (details.resourceType identifies what was missing)
102
+ 429 -> RATE_LIMIT_EXCEEDED (details.retryAfterSeconds carries Retry-After)
103
+ 500/503 -> EXECUTION_FAILED (retryable)
104
+ other -> EXECUTION_FAILED
105
+ """
106
+ graph_error = data.get("error") if isinstance(data, dict) else None
107
+ details: dict[str, Any] = {"statusCode": status, "graphError": graph_error, "resourceType": resource_type}
108
+
109
+ if status in (401, 403):
110
+ return MatimoError(
111
+ "Microsoft Graph access denied. Check connection status in Nova.",
112
+ ErrorCode.AUTH_FAILED,
113
+ details,
114
+ )
115
+
116
+ if status == 404:
117
+ return MatimoError(f"{resource_type} not found.", ErrorCode.FILE_NOT_FOUND, details)
118
+
119
+ if status == 429:
120
+ retry_after_header = None
121
+ if headers is not None:
122
+ retry_after_header = headers.get("retry-after") or headers.get("Retry-After")
123
+ retry_after_seconds = _parse_retry_after_seconds(retry_after_header)
124
+ return MatimoError(
125
+ "Microsoft Graph rate limit exceeded. Respect Retry-After before retrying.",
126
+ ErrorCode.RATE_LIMIT_EXCEEDED,
127
+ {**details, "retryAfterSeconds": retry_after_seconds},
128
+ )
129
+
130
+ if status in (500, 503):
131
+ return MatimoError(
132
+ "Microsoft Graph service is temporarily unavailable. Please retry shortly.",
133
+ ErrorCode.EXECUTION_FAILED,
134
+ details,
135
+ )
136
+
137
+ return MatimoError(
138
+ f"Microsoft Graph request failed with status {status}.",
139
+ ErrorCode.EXECUTION_FAILED,
140
+ details,
141
+ )
142
+
143
+
144
+ async def graph_request(
145
+ *,
146
+ method: HttpMethod,
147
+ path: str,
148
+ token: str,
149
+ query: dict[str, Any] | None = None,
150
+ body: Any = None,
151
+ headers: dict[str, str] | None = None,
152
+ resource_type: str = "Resource",
153
+ response_type: ResponseType = "json",
154
+ allow_empty_response: bool = False,
155
+ ) -> Any:
156
+ """
157
+ Perform an authenticated Microsoft Graph request with retry-on-429/5xx
158
+ (respecting Retry-After, exponential backoff, max 3 retries) and normalized
159
+ MatimoError mapping for every other failure.
160
+
161
+ Args:
162
+ path: Path relative to https://graph.microsoft.com/v1.0, e.g. '/me/messages'
163
+ response_type: 'bytes' for binary downloads (e.g. file content)
164
+ allow_empty_response: Treat a 204/empty body as success and return None
165
+ (e.g. publish, sendMail)
166
+ """
167
+ url = f"{GRAPH_BASE_URL}{path}"
168
+
169
+ is_json_body = body is not None and not isinstance(body, (bytes, bytearray))
170
+ request_headers: dict[str, str] = {
171
+ "Authorization": f"Bearer {token}",
172
+ **({"Accept": "application/json"} if response_type == "json" else {}),
173
+ **({"Content-Type": "application/json"} if is_json_body else {}),
174
+ **(headers or {}),
175
+ }
176
+
177
+ request_kwargs: dict[str, Any] = {}
178
+ if isinstance(body, (bytes, bytearray)):
179
+ request_kwargs["content"] = bytes(body)
180
+ elif body is not None:
181
+ request_kwargs["json"] = body
182
+
183
+ filtered_query = {
184
+ key: value
185
+ for key, value in (query or {}).items()
186
+ if value is not None and value != ""
187
+ }
188
+
189
+ attempt = 0
190
+ async with httpx.AsyncClient(timeout=30.0) as client:
191
+ while True:
192
+ try:
193
+ response = await client.request(
194
+ method,
195
+ url,
196
+ params=filtered_query or None,
197
+ headers=request_headers,
198
+ **request_kwargs,
199
+ )
200
+ except httpx.HTTPError as exc:
201
+ raise MatimoError(
202
+ "Microsoft Graph request failed before a response was received (network error).",
203
+ ErrorCode.NETWORK_ERROR,
204
+ {"path": path, "originalError": str(exc)},
205
+ cause=exc,
206
+ ) from exc
207
+
208
+ if 200 <= response.status_code < 300:
209
+ if allow_empty_response and (response.status_code == 204 or not response.content):
210
+ return None
211
+ if response_type == "bytes":
212
+ return response.content
213
+ if not response.content:
214
+ return None
215
+ return response.json()
216
+
217
+ try:
218
+ error_body: Any = response.json()
219
+ except ValueError:
220
+ error_body = None
221
+
222
+ error = map_graph_error(response.status_code, error_body, response.headers, resource_type)
223
+
224
+ is_retryable = response.status_code in _RETRYABLE_STATUS_CODES and attempt < _MAX_RETRIES
225
+ if not is_retryable:
226
+ raise error
227
+
228
+ retry_after_seconds = error.details.get("retryAfterSeconds") if error.details else None
229
+ delay_s = (
230
+ retry_after_seconds
231
+ if isinstance(retry_after_seconds, (int, float))
232
+ else _INITIAL_BACKOFF_S * (2**attempt)
233
+ )
234
+
235
+ attempt += 1
236
+ await asyncio.sleep(delay_s)
@@ -0,0 +1,100 @@
1
+ name: ms_create_calendar_event
2
+ description: |
3
+ Create an event on the signed-in user's default calendar (POST /me/events).
4
+ Supports attendees, location, a single IANA/Windows time zone for both start and
5
+ end, and Microsoft Teams online meetings (isOnlineMeeting), returning the meeting's
6
+ join URL when one is created.
7
+ version: '1.0.0'
8
+ status: approved
9
+ risk: medium
10
+
11
+ parameters:
12
+ subject:
13
+ type: string
14
+ description: Title of the event
15
+ required: true
16
+
17
+ body:
18
+ type: string
19
+ description: Description / agenda for the event (plain text)
20
+ required: false
21
+
22
+ start:
23
+ type: string
24
+ description: 'Start date and time, e.g. "2026-06-15T09:00:00"'
25
+ required: true
26
+
27
+ end:
28
+ type: string
29
+ description: 'End date and time, e.g. "2026-06-15T09:30:00"'
30
+ required: true
31
+
32
+ timezone:
33
+ type: string
34
+ description: 'IANA or Windows time zone applied to both `start` and `end`, e.g. "America/Los_Angeles"'
35
+ required: false
36
+ default: UTC
37
+
38
+ attendees:
39
+ type: array
40
+ description: Email addresses of attendees to invite
41
+ required: false
42
+
43
+ location:
44
+ type: string
45
+ description: Free-text location / room name for the event
46
+ required: false
47
+
48
+ is_online_meeting:
49
+ type: boolean
50
+ description: When true, Microsoft Teams creates an online meeting for this event
51
+ required: false
52
+ default: false
53
+
54
+ execution:
55
+ type: function
56
+ code: ./ms_create_calendar_event.py
57
+ timeout: 25000
58
+
59
+ authentication:
60
+ type: oauth2
61
+ provider: microsoft
62
+ scopes:
63
+ - https://graph.microsoft.com/Calendars.ReadWrite
64
+
65
+ output_schema:
66
+ type: object
67
+ properties:
68
+ success:
69
+ type: boolean
70
+ event_id:
71
+ type: string
72
+ web_link:
73
+ type: string
74
+ join_url:
75
+ type: string
76
+ description: Microsoft Teams meeting join URL — present only when is_online_meeting is true
77
+
78
+ error_handling:
79
+ retry: 1
80
+ backoff_type: exponential
81
+ initial_delay_ms: 1000
82
+
83
+ tags: [microsoft, graph, calendar, outlook, teams, write]
84
+
85
+ examples:
86
+ - name: Schedule a 30-minute internal sync
87
+ params:
88
+ subject: 'Sprint planning'
89
+ start: '2026-06-15T09:00:00'
90
+ end: '2026-06-15T09:30:00'
91
+ timezone: 'America/Los_Angeles'
92
+ attendees: ['alice@contoso.com', 'bob@contoso.com']
93
+ - name: Schedule an online Teams meeting with a location fallback
94
+ params:
95
+ subject: 'All-hands'
96
+ start: '2026-06-20T17:00:00'
97
+ end: '2026-06-20T18:00:00'
98
+ timezone: UTC
99
+ location: 'Building 4, Room 200'
100
+ is_online_meeting: true