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.
- matimo_microsoft-0.1.0/.gitignore +63 -0
- matimo_microsoft-0.1.0/PKG-INFO +119 -0
- matimo_microsoft-0.1.0/README.md +105 -0
- matimo_microsoft-0.1.0/pyproject.toml +27 -0
- matimo_microsoft-0.1.0/src/matimo_microsoft/__init__.py +17 -0
- matimo_microsoft-0.1.0/src/matimo_microsoft/graph_client.py +236 -0
- matimo_microsoft-0.1.0/src/matimo_microsoft/tools/ms_create_calendar_event/definition.yaml +100 -0
- matimo_microsoft-0.1.0/src/matimo_microsoft/tools/ms_create_calendar_event/ms_create_calendar_event.py +81 -0
- matimo_microsoft-0.1.0/src/matimo_microsoft/tools/ms_create_document/definition.yaml +103 -0
- matimo_microsoft-0.1.0/src/matimo_microsoft/tools/ms_create_document/ms_create_document.py +106 -0
- matimo_microsoft-0.1.0/src/matimo_microsoft/tools/ms_get_email/definition.yaml +88 -0
- matimo_microsoft-0.1.0/src/matimo_microsoft/tools/ms_get_email/ms_get_email.py +94 -0
- matimo_microsoft-0.1.0/src/matimo_microsoft/tools/ms_list_files/definition.yaml +81 -0
- matimo_microsoft-0.1.0/src/matimo_microsoft/tools/ms_list_files/ms_list_files.py +72 -0
- matimo_microsoft-0.1.0/src/matimo_microsoft/tools/ms_publish_to_sharepoint/definition.yaml +92 -0
- matimo_microsoft-0.1.0/src/matimo_microsoft/tools/ms_publish_to_sharepoint/ms_publish_to_sharepoint.py +122 -0
- matimo_microsoft-0.1.0/src/matimo_microsoft/tools/ms_read_file/definition.yaml +74 -0
- matimo_microsoft-0.1.0/src/matimo_microsoft/tools/ms_read_file/ms_read_file.py +96 -0
- matimo_microsoft-0.1.0/src/matimo_microsoft/tools/ms_search_knowledge/definition.yaml +99 -0
- matimo_microsoft-0.1.0/src/matimo_microsoft/tools/ms_search_knowledge/ms_search_knowledge.py +109 -0
- matimo_microsoft-0.1.0/src/matimo_microsoft/tools/ms_send_email/definition.yaml +94 -0
- matimo_microsoft-0.1.0/src/matimo_microsoft/tools/ms_send_email/ms_send_email.py +99 -0
- matimo_microsoft-0.1.0/src/matimo_microsoft/tools/ms_send_teams_message/definition.yaml +87 -0
- matimo_microsoft-0.1.0/src/matimo_microsoft/tools/ms_send_teams_message/ms_send_teams_message.py +59 -0
- matimo_microsoft-0.1.0/tests/unit/test_graph_client.py +328 -0
- 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
|
+
[](https://pypi.org/project/matimo-microsoft/)
|
|
21
|
+
[](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
|
+
[](https://pypi.org/project/matimo-microsoft/)
|
|
7
|
+
[](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
|