msgraph-mcp-server 0.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- msgraph_mcp/__init__.py +0 -0
- msgraph_mcp/auth/__init__.py +0 -0
- msgraph_mcp/auth/cli.py +45 -0
- msgraph_mcp/auth/msal_app.py +47 -0
- msgraph_mcp/auth/token.py +32 -0
- msgraph_mcp/config.py +79 -0
- msgraph_mcp/graph/__init__.py +0 -0
- msgraph_mcp/graph/auth_provider.py +29 -0
- msgraph_mcp/graph/batch.py +206 -0
- msgraph_mcp/graph/client.py +37 -0
- msgraph_mcp/graph/errors.py +43 -0
- msgraph_mcp/graph/pagination.py +47 -0
- msgraph_mcp/graph/serialize.py +351 -0
- msgraph_mcp/graph/trimming.py +359 -0
- msgraph_mcp/server.py +61 -0
- msgraph_mcp/tools/__init__.py +35 -0
- msgraph_mcp/tools/_binary.py +48 -0
- msgraph_mcp/tools/calendar.py +585 -0
- msgraph_mcp/tools/mail_actions.py +83 -0
- msgraph_mcp/tools/mail_batch.py +220 -0
- msgraph_mcp/tools/mail_folders.py +263 -0
- msgraph_mcp/tools/mail_read.py +333 -0
- msgraph_mcp/tools/mail_rules.py +468 -0
- msgraph_mcp/tools/mail_write.py +403 -0
- msgraph_mcp/tools/teams_channels.py +223 -0
- msgraph_mcp/tools/teams_chats.py +140 -0
- msgraph_mcp/tools/teams_content.py +150 -0
- msgraph_mcp/tools/util.py +35 -0
- msgraph_mcp_server-0.3.0.dist-info/METADATA +243 -0
- msgraph_mcp_server-0.3.0.dist-info/RECORD +33 -0
- msgraph_mcp_server-0.3.0.dist-info/WHEEL +4 -0
- msgraph_mcp_server-0.3.0.dist-info/entry_points.txt +3 -0
- msgraph_mcp_server-0.3.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
"""Batch mail-action tools backed by Microsoft Graph's $batch endpoint.
|
|
2
|
+
|
|
3
|
+
These are bulk versions of the single-message tools in mail_actions.py.
|
|
4
|
+
Each accepts a list of message ids and returns per-id success/error.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from msgraph_mcp.graph.batch import (
|
|
12
|
+
BatchRequest,
|
|
13
|
+
BatchResult,
|
|
14
|
+
GraphBatchTransport,
|
|
15
|
+
execute_batch,
|
|
16
|
+
)
|
|
17
|
+
from msgraph_mcp.graph.errors import GraphValidationError
|
|
18
|
+
|
|
19
|
+
MAX_BATCH_IDS = 1000
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _validate_ids(message_ids: list[str]) -> None:
|
|
23
|
+
if not message_ids:
|
|
24
|
+
raise GraphValidationError("`message_ids` must be a non-empty list")
|
|
25
|
+
if len(message_ids) > MAX_BATCH_IDS:
|
|
26
|
+
raise GraphValidationError(
|
|
27
|
+
f"`message_ids` exceeds maximum of {MAX_BATCH_IDS} per call"
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _mailbox_path_segment(mailbox: str | None) -> str:
|
|
32
|
+
"""Return the URL prefix for the mailbox: '/me' or '/users/{mailbox}'."""
|
|
33
|
+
return "/me" if mailbox is None else f"/users/{mailbox}"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _summarize(results: list[BatchResult]) -> dict[str, Any]:
|
|
37
|
+
out_results = [
|
|
38
|
+
{
|
|
39
|
+
"id": r.id,
|
|
40
|
+
"ok": r.ok,
|
|
41
|
+
**({"error": r.error} if r.error else {}),
|
|
42
|
+
}
|
|
43
|
+
for r in results
|
|
44
|
+
]
|
|
45
|
+
succeeded = sum(1 for r in results if r.ok)
|
|
46
|
+
return {
|
|
47
|
+
"results": out_results,
|
|
48
|
+
"summary": {
|
|
49
|
+
"total": len(results),
|
|
50
|
+
"succeeded": succeeded,
|
|
51
|
+
"failed": len(results) - succeeded,
|
|
52
|
+
},
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
async def batch_archive_messages(
|
|
57
|
+
*,
|
|
58
|
+
graph,
|
|
59
|
+
message_ids: list[str],
|
|
60
|
+
mailbox: str | None = None,
|
|
61
|
+
) -> dict[str, Any]:
|
|
62
|
+
"""Archive a list of messages (bulk move to the Archive well-known folder)."""
|
|
63
|
+
_validate_ids(message_ids)
|
|
64
|
+
base = _mailbox_path_segment(mailbox)
|
|
65
|
+
requests = [
|
|
66
|
+
BatchRequest(
|
|
67
|
+
id=mid,
|
|
68
|
+
method="POST",
|
|
69
|
+
url=f"{base}/messages/{mid}/move",
|
|
70
|
+
body={"destinationId": "archive"},
|
|
71
|
+
)
|
|
72
|
+
for mid in message_ids
|
|
73
|
+
]
|
|
74
|
+
transport = GraphBatchTransport(graph)
|
|
75
|
+
results = await execute_batch(transport, requests)
|
|
76
|
+
return _summarize(results)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
async def batch_move_messages(
|
|
80
|
+
*,
|
|
81
|
+
graph,
|
|
82
|
+
message_ids: list[str],
|
|
83
|
+
destination: str,
|
|
84
|
+
mailbox: str | None = None,
|
|
85
|
+
) -> dict[str, Any]:
|
|
86
|
+
"""Move a list of messages to the given folder.
|
|
87
|
+
|
|
88
|
+
`destination` accepts a folder id OR one of the well-known names that
|
|
89
|
+
`move_message` already supports ('archive', 'inbox', 'junkemail',
|
|
90
|
+
'deleteditems', 'sentitems', 'drafts'). Graph resolves well-known names
|
|
91
|
+
server-side, so we pass through without local resolution.
|
|
92
|
+
|
|
93
|
+
Spec deviation: Spec §6.3 calls for pre-resolving well-known names to
|
|
94
|
+
folder ids. In practice Graph's /move endpoint accepts well-known names
|
|
95
|
+
directly (this is how the existing single-message move_message already
|
|
96
|
+
works — see src/msgraph_mcp/tools/mail_folders.py). We pass through
|
|
97
|
+
unchanged. If a real-world test surfaces a case where Graph rejects a
|
|
98
|
+
well-known name in $batch but accepts it in single calls, add resolution
|
|
99
|
+
then.
|
|
100
|
+
"""
|
|
101
|
+
_validate_ids(message_ids)
|
|
102
|
+
if not destination:
|
|
103
|
+
raise GraphValidationError("`destination` is required")
|
|
104
|
+
base = _mailbox_path_segment(mailbox)
|
|
105
|
+
requests = [
|
|
106
|
+
BatchRequest(
|
|
107
|
+
id=mid,
|
|
108
|
+
method="POST",
|
|
109
|
+
url=f"{base}/messages/{mid}/move",
|
|
110
|
+
body={"destinationId": destination},
|
|
111
|
+
)
|
|
112
|
+
for mid in message_ids
|
|
113
|
+
]
|
|
114
|
+
transport = GraphBatchTransport(graph)
|
|
115
|
+
results = await execute_batch(transport, requests)
|
|
116
|
+
return _summarize(results)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _patch_requests(
|
|
120
|
+
message_ids: list[str],
|
|
121
|
+
mailbox: str | None,
|
|
122
|
+
body: dict[str, Any],
|
|
123
|
+
) -> list[BatchRequest]:
|
|
124
|
+
"""Build a list of PATCH BatchRequests for /me/messages/{id} (no /move)."""
|
|
125
|
+
base = _mailbox_path_segment(mailbox)
|
|
126
|
+
return [
|
|
127
|
+
BatchRequest(
|
|
128
|
+
id=mid,
|
|
129
|
+
method="PATCH",
|
|
130
|
+
url=f"{base}/messages/{mid}",
|
|
131
|
+
body=body,
|
|
132
|
+
)
|
|
133
|
+
for mid in message_ids
|
|
134
|
+
]
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
async def batch_mark_read(
|
|
138
|
+
*,
|
|
139
|
+
graph,
|
|
140
|
+
message_ids: list[str],
|
|
141
|
+
mailbox: str | None = None,
|
|
142
|
+
) -> dict[str, Any]:
|
|
143
|
+
"""Mark a list of messages as read."""
|
|
144
|
+
_validate_ids(message_ids)
|
|
145
|
+
requests = _patch_requests(message_ids, mailbox, {"isRead": True})
|
|
146
|
+
transport = GraphBatchTransport(graph)
|
|
147
|
+
results = await execute_batch(transport, requests)
|
|
148
|
+
return _summarize(results)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
async def batch_mark_unread(
|
|
152
|
+
*,
|
|
153
|
+
graph,
|
|
154
|
+
message_ids: list[str],
|
|
155
|
+
mailbox: str | None = None,
|
|
156
|
+
) -> dict[str, Any]:
|
|
157
|
+
"""Mark a list of messages as unread."""
|
|
158
|
+
_validate_ids(message_ids)
|
|
159
|
+
requests = _patch_requests(message_ids, mailbox, {"isRead": False})
|
|
160
|
+
transport = GraphBatchTransport(graph)
|
|
161
|
+
results = await execute_batch(transport, requests)
|
|
162
|
+
return _summarize(results)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
async def batch_flag_messages(
|
|
166
|
+
*,
|
|
167
|
+
graph,
|
|
168
|
+
message_ids: list[str],
|
|
169
|
+
mailbox: str | None = None,
|
|
170
|
+
) -> dict[str, Any]:
|
|
171
|
+
"""Flag a list of messages (set followup flag to 'flagged')."""
|
|
172
|
+
_validate_ids(message_ids)
|
|
173
|
+
requests = _patch_requests(message_ids, mailbox, {"flag": {"flagStatus": "flagged"}})
|
|
174
|
+
transport = GraphBatchTransport(graph)
|
|
175
|
+
results = await execute_batch(transport, requests)
|
|
176
|
+
return _summarize(results)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
async def batch_unflag_messages(
|
|
180
|
+
*,
|
|
181
|
+
graph,
|
|
182
|
+
message_ids: list[str],
|
|
183
|
+
mailbox: str | None = None,
|
|
184
|
+
) -> dict[str, Any]:
|
|
185
|
+
"""Unflag a list of messages (set followup flag to 'notFlagged')."""
|
|
186
|
+
_validate_ids(message_ids)
|
|
187
|
+
requests = _patch_requests(message_ids, mailbox, {"flag": {"flagStatus": "notFlagged"}})
|
|
188
|
+
transport = GraphBatchTransport(graph)
|
|
189
|
+
results = await execute_batch(transport, requests)
|
|
190
|
+
return _summarize(results)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def register(mcp, *, graph) -> None:
|
|
194
|
+
@mcp.tool(name="batch_archive_messages", description=batch_archive_messages.__doc__ or "")
|
|
195
|
+
async def _archive(message_ids: list[str], mailbox: str | None = None):
|
|
196
|
+
return await batch_archive_messages(
|
|
197
|
+
graph=graph, message_ids=message_ids, mailbox=mailbox
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
@mcp.tool(name="batch_move_messages", description=batch_move_messages.__doc__ or "")
|
|
201
|
+
async def _move(message_ids: list[str], destination: str, mailbox: str | None = None):
|
|
202
|
+
return await batch_move_messages(
|
|
203
|
+
graph=graph, message_ids=message_ids, destination=destination, mailbox=mailbox
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
@mcp.tool(name="batch_mark_read", description=batch_mark_read.__doc__ or "")
|
|
207
|
+
async def _mark_read(message_ids: list[str], mailbox: str | None = None):
|
|
208
|
+
return await batch_mark_read(graph=graph, message_ids=message_ids, mailbox=mailbox)
|
|
209
|
+
|
|
210
|
+
@mcp.tool(name="batch_mark_unread", description=batch_mark_unread.__doc__ or "")
|
|
211
|
+
async def _mark_unread(message_ids: list[str], mailbox: str | None = None):
|
|
212
|
+
return await batch_mark_unread(graph=graph, message_ids=message_ids, mailbox=mailbox)
|
|
213
|
+
|
|
214
|
+
@mcp.tool(name="batch_flag_messages", description=batch_flag_messages.__doc__ or "")
|
|
215
|
+
async def _flag(message_ids: list[str], mailbox: str | None = None):
|
|
216
|
+
return await batch_flag_messages(graph=graph, message_ids=message_ids, mailbox=mailbox)
|
|
217
|
+
|
|
218
|
+
@mcp.tool(name="batch_unflag_messages", description=batch_unflag_messages.__doc__ or "")
|
|
219
|
+
async def _unflag(message_ids: list[str], mailbox: str | None = None):
|
|
220
|
+
return await batch_unflag_messages(graph=graph, message_ids=message_ids, mailbox=mailbox)
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
"""Mail folder + move tools."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from msgraph.generated.models.mail_folder import MailFolder
|
|
6
|
+
from msgraph.generated.users.item.mail_folders.item.move.move_post_request_body import (
|
|
7
|
+
MovePostRequestBody as FolderMovePostRequestBody,
|
|
8
|
+
)
|
|
9
|
+
from msgraph.generated.users.item.messages.item.move.move_post_request_body import (
|
|
10
|
+
MovePostRequestBody,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
from msgraph_mcp.auth.token import NotAuthenticatedError
|
|
14
|
+
from msgraph_mcp.graph.errors import GraphValidationError, map_kiota_error
|
|
15
|
+
from msgraph_mcp.graph.serialize import folder_to_dict, message_to_dict
|
|
16
|
+
from msgraph_mcp.graph.trimming import trim_folder, trim_message
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
async def list_folders(
|
|
20
|
+
*,
|
|
21
|
+
graph,
|
|
22
|
+
mailbox: str | None = None,
|
|
23
|
+
include_raw: bool = False,
|
|
24
|
+
) -> dict:
|
|
25
|
+
"""List the user's mail folders (top level).
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
{"items": [trimmed_folder, ...], "next_page_token": None}
|
|
29
|
+
"""
|
|
30
|
+
try:
|
|
31
|
+
collection = await graph.mailbox(mailbox).mail_folders.get()
|
|
32
|
+
except NotAuthenticatedError:
|
|
33
|
+
raise
|
|
34
|
+
except Exception as exc: # noqa: BLE001
|
|
35
|
+
raise map_kiota_error(exc) from exc
|
|
36
|
+
items = [trim_folder(folder_to_dict(f), include_raw=include_raw) for f in (collection.value or [])]
|
|
37
|
+
return {"items": items, "next_page_token": None}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
async def move_message(
|
|
41
|
+
*,
|
|
42
|
+
graph,
|
|
43
|
+
message_id: str,
|
|
44
|
+
destination: str,
|
|
45
|
+
mailbox: str | None = None,
|
|
46
|
+
include_raw: bool = False,
|
|
47
|
+
) -> dict:
|
|
48
|
+
"""Move a message to another folder.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
message_id: Graph message id.
|
|
52
|
+
destination: Target folder id, OR one of the well-known names:
|
|
53
|
+
'archive', 'inbox', 'junkemail', 'deleteditems', 'sentitems', 'drafts'.
|
|
54
|
+
mailbox: Optional mailbox.
|
|
55
|
+
include_raw: Include the raw Graph payload.
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
The trimmed moved message (which lives in the destination folder).
|
|
59
|
+
"""
|
|
60
|
+
if not destination:
|
|
61
|
+
raise GraphValidationError("`destination` is required")
|
|
62
|
+
body = MovePostRequestBody()
|
|
63
|
+
body.destination_id = destination
|
|
64
|
+
try:
|
|
65
|
+
moved = await (
|
|
66
|
+
graph.mailbox(mailbox).messages.by_message_id(message_id).move.post(body)
|
|
67
|
+
)
|
|
68
|
+
except NotAuthenticatedError:
|
|
69
|
+
raise
|
|
70
|
+
except Exception as exc: # noqa: BLE001
|
|
71
|
+
raise map_kiota_error(exc) from exc
|
|
72
|
+
return trim_message(message_to_dict(moved), include_body=False, include_raw=include_raw)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
async def create_folder(
|
|
76
|
+
*,
|
|
77
|
+
graph,
|
|
78
|
+
display_name: str,
|
|
79
|
+
parent_folder_id: str | None = None,
|
|
80
|
+
mailbox: str | None = None,
|
|
81
|
+
include_raw: bool = False,
|
|
82
|
+
) -> dict:
|
|
83
|
+
"""Create a new mail folder (top-level or child).
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
display_name: Folder name. Required, non-empty.
|
|
87
|
+
parent_folder_id: If set, create as a child of this folder; else
|
|
88
|
+
create at the top level. Accepts a folder id or a well-known
|
|
89
|
+
name ('inbox', 'archive', etc.).
|
|
90
|
+
mailbox: Optional mailbox (email or user id) for shared mailboxes.
|
|
91
|
+
include_raw: Include the raw Graph payload.
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
The trimmed new folder.
|
|
95
|
+
"""
|
|
96
|
+
if not display_name or not display_name.strip():
|
|
97
|
+
raise GraphValidationError("`display_name` is required")
|
|
98
|
+
body = MailFolder()
|
|
99
|
+
body.display_name = display_name
|
|
100
|
+
try:
|
|
101
|
+
if parent_folder_id:
|
|
102
|
+
created = await (
|
|
103
|
+
graph.mailbox(mailbox)
|
|
104
|
+
.mail_folders.by_mail_folder_id(parent_folder_id)
|
|
105
|
+
.child_folders.post(body)
|
|
106
|
+
)
|
|
107
|
+
else:
|
|
108
|
+
created = await graph.mailbox(mailbox).mail_folders.post(body)
|
|
109
|
+
except NotAuthenticatedError:
|
|
110
|
+
raise
|
|
111
|
+
except Exception as exc: # noqa: BLE001
|
|
112
|
+
raise map_kiota_error(exc) from exc
|
|
113
|
+
return trim_folder(folder_to_dict(created), include_raw=include_raw)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
async def update_folder(
|
|
117
|
+
*,
|
|
118
|
+
graph,
|
|
119
|
+
folder_id: str,
|
|
120
|
+
display_name: str | None = None,
|
|
121
|
+
parent_folder_id: str | None = None,
|
|
122
|
+
mailbox: str | None = None,
|
|
123
|
+
include_raw: bool = False,
|
|
124
|
+
) -> dict:
|
|
125
|
+
"""Rename and/or reparent a mail folder.
|
|
126
|
+
|
|
127
|
+
At least one of ``display_name`` or ``parent_folder_id`` is required.
|
|
128
|
+
Well-known folders (inbox, drafts, sent items, etc.) cannot be
|
|
129
|
+
renamed or moved — Graph will reject the request.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
folder_id: Graph folder id. Required.
|
|
133
|
+
display_name: New folder name. None = unchanged.
|
|
134
|
+
parent_folder_id: Move under this folder. Accepts a folder id or
|
|
135
|
+
a well-known name ('inbox', 'archive', etc.). None = unchanged.
|
|
136
|
+
mailbox: Optional mailbox (email or user id) for shared mailboxes.
|
|
137
|
+
include_raw: Include the raw Graph payload.
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
The trimmed updated folder.
|
|
141
|
+
"""
|
|
142
|
+
if not folder_id:
|
|
143
|
+
raise GraphValidationError("`folder_id` is required")
|
|
144
|
+
if display_name is None and parent_folder_id is None:
|
|
145
|
+
raise GraphValidationError(
|
|
146
|
+
"At least one of `display_name` or `parent_folder_id` is required"
|
|
147
|
+
)
|
|
148
|
+
if display_name is not None and not display_name.strip():
|
|
149
|
+
raise GraphValidationError("`display_name` cannot be empty")
|
|
150
|
+
|
|
151
|
+
folder_endpoint = graph.mailbox(mailbox).mail_folders.by_mail_folder_id(folder_id)
|
|
152
|
+
result = None
|
|
153
|
+
try:
|
|
154
|
+
if display_name is not None:
|
|
155
|
+
patch = MailFolder()
|
|
156
|
+
patch.display_name = display_name
|
|
157
|
+
result = await folder_endpoint.patch(patch)
|
|
158
|
+
if parent_folder_id is not None:
|
|
159
|
+
move_body = FolderMovePostRequestBody()
|
|
160
|
+
move_body.destination_id = parent_folder_id
|
|
161
|
+
result = await folder_endpoint.move.post(move_body)
|
|
162
|
+
except NotAuthenticatedError:
|
|
163
|
+
raise
|
|
164
|
+
except Exception as exc: # noqa: BLE001
|
|
165
|
+
raise map_kiota_error(exc) from exc
|
|
166
|
+
return trim_folder(folder_to_dict(result), include_raw=include_raw)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
async def delete_folder(
|
|
170
|
+
*,
|
|
171
|
+
graph,
|
|
172
|
+
folder_id: str,
|
|
173
|
+
mailbox: str | None = None,
|
|
174
|
+
) -> dict:
|
|
175
|
+
"""Delete a mail folder. Refuses to delete folders that aren't empty.
|
|
176
|
+
|
|
177
|
+
Reads the folder first and aborts if `totalItemCount > 0` or
|
|
178
|
+
`childFolderCount > 0`. This is a small safety net against accidentally
|
|
179
|
+
nuking a folder with real messages; there is a small race window between
|
|
180
|
+
the read and the delete, so don't rely on this for adversarial safety.
|
|
181
|
+
|
|
182
|
+
Args:
|
|
183
|
+
folder_id: Graph folder id. Required.
|
|
184
|
+
mailbox: Optional mailbox (email or user id) for shared mailboxes.
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
{"deleted": True, "id": <folder_id>}
|
|
188
|
+
"""
|
|
189
|
+
if not folder_id:
|
|
190
|
+
raise GraphValidationError("`folder_id` is required")
|
|
191
|
+
folder_endpoint = graph.mailbox(mailbox).mail_folders.by_mail_folder_id(folder_id)
|
|
192
|
+
try:
|
|
193
|
+
folder = await folder_endpoint.get()
|
|
194
|
+
except NotAuthenticatedError:
|
|
195
|
+
raise
|
|
196
|
+
except Exception as exc: # noqa: BLE001
|
|
197
|
+
raise map_kiota_error(exc) from exc
|
|
198
|
+
total = getattr(folder, "total_item_count", None) or 0
|
|
199
|
+
children = getattr(folder, "child_folder_count", None) or 0
|
|
200
|
+
if total > 0 or children > 0:
|
|
201
|
+
raise GraphValidationError(
|
|
202
|
+
f"Refusing to delete non-empty folder {folder_id!r}: "
|
|
203
|
+
f"contains {total} message(s) and {children} child folder(s). "
|
|
204
|
+
"Empty it first or move the contents elsewhere."
|
|
205
|
+
)
|
|
206
|
+
try:
|
|
207
|
+
await folder_endpoint.delete()
|
|
208
|
+
except NotAuthenticatedError:
|
|
209
|
+
raise
|
|
210
|
+
except Exception as exc: # noqa: BLE001
|
|
211
|
+
raise map_kiota_error(exc) from exc
|
|
212
|
+
return {"deleted": True, "id": folder_id}
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def register(mcp, *, graph) -> None:
|
|
216
|
+
@mcp.tool(name="list_folders", description=list_folders.__doc__ or "")
|
|
217
|
+
async def _list_folders(mailbox: str | None = None, include_raw: bool = False):
|
|
218
|
+
return await list_folders(graph=graph, mailbox=mailbox, include_raw=include_raw)
|
|
219
|
+
|
|
220
|
+
@mcp.tool(name="move_message", description=move_message.__doc__ or "")
|
|
221
|
+
async def _move_message(
|
|
222
|
+
message_id: str, destination: str, mailbox: str | None = None, include_raw: bool = False
|
|
223
|
+
):
|
|
224
|
+
return await move_message(
|
|
225
|
+
graph=graph, message_id=message_id, destination=destination,
|
|
226
|
+
mailbox=mailbox, include_raw=include_raw,
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
@mcp.tool(name="create_folder", description=create_folder.__doc__ or "")
|
|
230
|
+
async def _create_folder(
|
|
231
|
+
display_name: str,
|
|
232
|
+
parent_folder_id: str | None = None,
|
|
233
|
+
mailbox: str | None = None,
|
|
234
|
+
include_raw: bool = False,
|
|
235
|
+
):
|
|
236
|
+
return await create_folder(
|
|
237
|
+
graph=graph,
|
|
238
|
+
display_name=display_name,
|
|
239
|
+
parent_folder_id=parent_folder_id,
|
|
240
|
+
mailbox=mailbox,
|
|
241
|
+
include_raw=include_raw,
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
@mcp.tool(name="update_folder", description=update_folder.__doc__ or "")
|
|
245
|
+
async def _update_folder(
|
|
246
|
+
folder_id: str,
|
|
247
|
+
display_name: str | None = None,
|
|
248
|
+
parent_folder_id: str | None = None,
|
|
249
|
+
mailbox: str | None = None,
|
|
250
|
+
include_raw: bool = False,
|
|
251
|
+
):
|
|
252
|
+
return await update_folder(
|
|
253
|
+
graph=graph,
|
|
254
|
+
folder_id=folder_id,
|
|
255
|
+
display_name=display_name,
|
|
256
|
+
parent_folder_id=parent_folder_id,
|
|
257
|
+
mailbox=mailbox,
|
|
258
|
+
include_raw=include_raw,
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
@mcp.tool(name="delete_folder", description=delete_folder.__doc__ or "")
|
|
262
|
+
async def _delete_folder(folder_id: str, mailbox: str | None = None):
|
|
263
|
+
return await delete_folder(graph=graph, folder_id=folder_id, mailbox=mailbox)
|