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.
@@ -0,0 +1,403 @@
1
+ """Mail write tools: send, draft, reply, reply_all, forward, update, delete."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import base64
6
+ from typing import Literal
7
+
8
+ from msgraph.generated.models.attachment import Attachment
9
+ from msgraph.generated.models.body_type import BodyType
10
+ from msgraph.generated.models.email_address import EmailAddress
11
+ from msgraph.generated.models.file_attachment import FileAttachment
12
+ from msgraph.generated.models.followup_flag import FollowupFlag
13
+ from msgraph.generated.models.followup_flag_status import FollowupFlagStatus
14
+ from msgraph.generated.models.importance import Importance
15
+ from msgraph.generated.models.item_body import ItemBody
16
+ from msgraph.generated.models.message import Message
17
+ from msgraph.generated.models.recipient import Recipient
18
+ from msgraph.generated.users.item.messages.item.forward.forward_post_request_body import (
19
+ ForwardPostRequestBody,
20
+ )
21
+ from msgraph.generated.users.item.messages.item.reply.reply_post_request_body import (
22
+ ReplyPostRequestBody,
23
+ )
24
+ from msgraph.generated.users.item.messages.item.reply_all.reply_all_post_request_body import (
25
+ ReplyAllPostRequestBody,
26
+ )
27
+ from msgraph.generated.users.item.send_mail.send_mail_post_request_body import (
28
+ SendMailPostRequestBody,
29
+ )
30
+
31
+ from msgraph_mcp.auth.token import NotAuthenticatedError
32
+ from msgraph_mcp.graph.errors import GraphValidationError, map_kiota_error
33
+ from msgraph_mcp.graph.serialize import message_to_dict
34
+ from msgraph_mcp.graph.trimming import trim_message
35
+
36
+
37
+ _MAX_INLINE_ATTACHMENT_BYTES = 3 * 1024 * 1024
38
+
39
+
40
+ def _recipient(addr: str) -> Recipient:
41
+ r = Recipient()
42
+ e = EmailAddress()
43
+ e.address = addr
44
+ r.email_address = e
45
+ return r
46
+
47
+
48
+ def _recipients(addrs: list[str] | None) -> list[Recipient]:
49
+ return [_recipient(a) for a in (addrs or [])]
50
+
51
+
52
+ def _body(text: str, body_type: Literal["text", "html"]) -> ItemBody:
53
+ b = ItemBody()
54
+ b.content_type = BodyType.Text if body_type == "text" else BodyType.Html
55
+ b.content = text
56
+ return b
57
+
58
+
59
+ def _validate_and_build_attachments(atts: list[dict] | None) -> list[Attachment]:
60
+ if not atts:
61
+ return []
62
+ total = 0
63
+ built: list[Attachment] = []
64
+ for spec in atts:
65
+ try:
66
+ raw = base64.b64decode(spec["content_b64"], validate=True)
67
+ except (KeyError, ValueError) as exc:
68
+ raise GraphValidationError(f"Invalid attachment content_b64: {exc}") from exc
69
+ total += len(raw)
70
+ if total > _MAX_INLINE_ATTACHMENT_BYTES:
71
+ raise GraphValidationError(
72
+ f"Attachment payload exceeds 3 MB raw limit (got {total} bytes). "
73
+ "Use a chunked upload (not supported in v1) for larger files."
74
+ )
75
+ fa = FileAttachment()
76
+ fa.name = spec.get("name") or "attachment"
77
+ fa.content_type = spec.get("content_type") or "application/octet-stream"
78
+ fa.content_bytes = raw
79
+ fa.odata_type = "#microsoft.graph.fileAttachment"
80
+ built.append(fa)
81
+ return built
82
+
83
+
84
+ def _build_message(
85
+ *,
86
+ to: list[str],
87
+ subject: str,
88
+ body: str,
89
+ body_type: Literal["text", "html"],
90
+ cc: list[str] | None = None,
91
+ bcc: list[str] | None = None,
92
+ attachments: list[dict] | None = None,
93
+ ) -> Message:
94
+ m = Message()
95
+ m.subject = subject
96
+ m.body = _body(body, body_type)
97
+ m.to_recipients = _recipients(to)
98
+ m.cc_recipients = _recipients(cc)
99
+ m.bcc_recipients = _recipients(bcc)
100
+ atts = _validate_and_build_attachments(attachments)
101
+ if atts:
102
+ m.attachments = atts
103
+ return m
104
+
105
+
106
+ async def send_message(
107
+ *,
108
+ graph,
109
+ to: list[str],
110
+ subject: str,
111
+ body: str,
112
+ body_type: Literal["text", "html"] = "text",
113
+ cc: list[str] | None = None,
114
+ bcc: list[str] | None = None,
115
+ attachments: list[dict] | None = None,
116
+ save_to_sent_items: bool = True,
117
+ mailbox: str | None = None,
118
+ ) -> dict:
119
+ """Send a new message immediately.
120
+
121
+ Args:
122
+ to: List of recipient email addresses (required, non-empty).
123
+ subject: Subject line.
124
+ body: Message body.
125
+ body_type: "text" or "html". Default "text".
126
+ cc: Optional CC list.
127
+ bcc: Optional BCC list.
128
+ attachments: Optional list of {name, content_b64, content_type}.
129
+ Total raw size must be <= 3 MB.
130
+ save_to_sent_items: Default True.
131
+ mailbox: Optional mailbox (defaults to signed-in user).
132
+
133
+ Returns:
134
+ {"status": "sent"}
135
+ """
136
+ if not to:
137
+ raise GraphValidationError("`to` must be a non-empty list of email addresses")
138
+ msg = _build_message(
139
+ to=to, subject=subject, body=body, body_type=body_type,
140
+ cc=cc, bcc=bcc, attachments=attachments,
141
+ )
142
+ req = SendMailPostRequestBody()
143
+ req.message = msg
144
+ req.save_to_sent_items = save_to_sent_items
145
+ try:
146
+ await graph.mailbox(mailbox).send_mail.post(req)
147
+ except NotAuthenticatedError:
148
+ raise
149
+ except Exception as exc: # noqa: BLE001
150
+ raise map_kiota_error(exc) from exc
151
+ return {"status": "sent"}
152
+
153
+
154
+ async def create_draft(
155
+ *,
156
+ graph,
157
+ to: list[str],
158
+ subject: str,
159
+ body: str,
160
+ body_type: Literal["text", "html"] = "text",
161
+ cc: list[str] | None = None,
162
+ bcc: list[str] | None = None,
163
+ attachments: list[dict] | None = None,
164
+ mailbox: str | None = None,
165
+ include_raw: bool = False,
166
+ ) -> dict:
167
+ """Create a draft message (not sent). Returns the trimmed draft."""
168
+ msg = _build_message(
169
+ to=to, subject=subject, body=body, body_type=body_type,
170
+ cc=cc, bcc=bcc, attachments=attachments,
171
+ )
172
+ try:
173
+ created = await graph.mailbox(mailbox).messages.post(msg)
174
+ except NotAuthenticatedError:
175
+ raise
176
+ except Exception as exc: # noqa: BLE001
177
+ raise map_kiota_error(exc) from exc
178
+ return trim_message(message_to_dict(created), include_body=False, include_raw=include_raw)
179
+
180
+
181
+ def _reply_body(comment: str | None, to: list[str] | None) -> ReplyPostRequestBody:
182
+ body = ReplyPostRequestBody()
183
+ if comment is not None:
184
+ body.comment = comment
185
+ if to:
186
+ m = Message()
187
+ m.to_recipients = _recipients(to)
188
+ body.message = m
189
+ return body
190
+
191
+
192
+ def _reply_all_body(comment: str | None) -> ReplyAllPostRequestBody:
193
+ body = ReplyAllPostRequestBody()
194
+ if comment is not None:
195
+ body.comment = comment
196
+ return body
197
+
198
+
199
+ def _forward_body(comment: str | None, to: list[str]) -> ForwardPostRequestBody:
200
+ body = ForwardPostRequestBody()
201
+ if comment is not None:
202
+ body.comment = comment
203
+ body.to_recipients = _recipients(to)
204
+ return body
205
+
206
+
207
+ async def reply_message(
208
+ *,
209
+ graph,
210
+ message_id: str,
211
+ comment: str | None = None,
212
+ extra_to: list[str] | None = None,
213
+ mailbox: str | None = None,
214
+ ) -> dict:
215
+ """Reply to the sender of a message."""
216
+ try:
217
+ await (
218
+ graph.mailbox(mailbox)
219
+ .messages.by_message_id(message_id)
220
+ .reply.post(_reply_body(comment, extra_to))
221
+ )
222
+ except NotAuthenticatedError:
223
+ raise
224
+ except Exception as exc: # noqa: BLE001
225
+ raise map_kiota_error(exc) from exc
226
+ return {"status": "sent"}
227
+
228
+
229
+ async def reply_all_message(
230
+ *,
231
+ graph,
232
+ message_id: str,
233
+ comment: str | None = None,
234
+ mailbox: str | None = None,
235
+ ) -> dict:
236
+ """Reply-all to a message (sender + everyone on the to/cc lines)."""
237
+ try:
238
+ await (
239
+ graph.mailbox(mailbox)
240
+ .messages.by_message_id(message_id)
241
+ .reply_all.post(_reply_all_body(comment))
242
+ )
243
+ except NotAuthenticatedError:
244
+ raise
245
+ except Exception as exc: # noqa: BLE001
246
+ raise map_kiota_error(exc) from exc
247
+ return {"status": "sent"}
248
+
249
+
250
+ async def forward_message(
251
+ *,
252
+ graph,
253
+ message_id: str,
254
+ to: list[str],
255
+ comment: str | None = None,
256
+ mailbox: str | None = None,
257
+ ) -> dict:
258
+ """Forward a message to new recipients."""
259
+ if not to:
260
+ raise GraphValidationError("`to` must be a non-empty list of email addresses")
261
+ try:
262
+ await (
263
+ graph.mailbox(mailbox)
264
+ .messages.by_message_id(message_id)
265
+ .forward.post(_forward_body(comment, to))
266
+ )
267
+ except NotAuthenticatedError:
268
+ raise
269
+ except Exception as exc: # noqa: BLE001
270
+ raise map_kiota_error(exc) from exc
271
+ return {"status": "sent"}
272
+
273
+
274
+ _IMPORTANCE_MAP = {"low": Importance.Low, "normal": Importance.Normal, "high": Importance.High}
275
+ _FLAG_MAP = {
276
+ "notFlagged": FollowupFlagStatus.NotFlagged,
277
+ "flagged": FollowupFlagStatus.Flagged,
278
+ "complete": FollowupFlagStatus.Complete,
279
+ }
280
+
281
+
282
+ async def update_message(
283
+ *,
284
+ graph,
285
+ message_id: str,
286
+ is_read: bool | None = None,
287
+ flag: Literal["notFlagged", "flagged", "complete"] | None = None,
288
+ importance: Literal["low", "normal", "high"] | None = None,
289
+ categories: list[str] | None = None,
290
+ parent_folder_id: str | None = None,
291
+ mailbox: str | None = None,
292
+ include_raw: bool = False,
293
+ ) -> dict:
294
+ """Patch a message's read state / flag / importance / categories.
295
+
296
+ For moving a message between folders, prefer `move_message` (clearer
297
+ intent). This tool patches in-place; setting parent_folder_id here
298
+ works but is not the primary path.
299
+
300
+ Returns the trimmed updated message.
301
+ """
302
+ patch = Message()
303
+ if is_read is not None:
304
+ patch.is_read = is_read
305
+ if flag is not None:
306
+ if flag not in _FLAG_MAP:
307
+ raise GraphValidationError(f"flag must be one of {list(_FLAG_MAP)}")
308
+ ff = FollowupFlag()
309
+ ff.flag_status = _FLAG_MAP[flag]
310
+ patch.flag = ff
311
+ if importance is not None:
312
+ if importance not in _IMPORTANCE_MAP:
313
+ raise GraphValidationError(f"importance must be one of {list(_IMPORTANCE_MAP)}")
314
+ patch.importance = _IMPORTANCE_MAP[importance]
315
+ if categories is not None:
316
+ patch.categories = list(categories)
317
+ if parent_folder_id is not None:
318
+ patch.parent_folder_id = parent_folder_id
319
+
320
+ try:
321
+ updated = await graph.mailbox(mailbox).messages.by_message_id(message_id).patch(patch)
322
+ except NotAuthenticatedError:
323
+ raise
324
+ except Exception as exc: # noqa: BLE001
325
+ raise map_kiota_error(exc) from exc
326
+ return trim_message(message_to_dict(updated), include_body=False, include_raw=include_raw)
327
+
328
+
329
+ async def delete_message(
330
+ *, graph, message_id: str, mailbox: str | None = None
331
+ ) -> dict:
332
+ """Soft-delete a message (moves it to Deleted Items)."""
333
+ try:
334
+ await graph.mailbox(mailbox).messages.by_message_id(message_id).delete()
335
+ except NotAuthenticatedError:
336
+ raise
337
+ except Exception as exc: # noqa: BLE001
338
+ raise map_kiota_error(exc) from exc
339
+ return {"status": "deleted"}
340
+
341
+
342
+ def register(mcp, *, graph) -> None:
343
+ @mcp.tool(name="send_message", description=send_message.__doc__ or "")
344
+ async def _send(
345
+ to: list[str], subject: str, body: str,
346
+ body_type: Literal["text", "html"] = "text",
347
+ cc: list[str] | None = None,
348
+ bcc: list[str] | None = None, attachments: list[dict] | None = None,
349
+ save_to_sent_items: bool = True, mailbox: str | None = None,
350
+ ):
351
+ return await send_message(
352
+ graph=graph, to=to, subject=subject, body=body,
353
+ body_type=body_type, cc=cc, bcc=bcc, attachments=attachments,
354
+ save_to_sent_items=save_to_sent_items, mailbox=mailbox,
355
+ )
356
+
357
+ @mcp.tool(name="create_draft", description=create_draft.__doc__ or "")
358
+ async def _draft(
359
+ to: list[str], subject: str, body: str,
360
+ body_type: Literal["text", "html"] = "text",
361
+ cc: list[str] | None = None,
362
+ bcc: list[str] | None = None, attachments: list[dict] | None = None,
363
+ mailbox: str | None = None, include_raw: bool = False,
364
+ ):
365
+ return await create_draft(
366
+ graph=graph, to=to, subject=subject, body=body,
367
+ body_type=body_type, cc=cc, bcc=bcc, attachments=attachments,
368
+ mailbox=mailbox, include_raw=include_raw,
369
+ )
370
+
371
+ @mcp.tool(name="reply_message", description=reply_message.__doc__ or "")
372
+ async def _reply(message_id: str, comment: str | None = None, extra_to: list[str] | None = None, mailbox: str | None = None):
373
+ return await reply_message(graph=graph, message_id=message_id, comment=comment, extra_to=extra_to, mailbox=mailbox)
374
+
375
+ @mcp.tool(name="reply_all_message", description=reply_all_message.__doc__ or "")
376
+ async def _reply_all(message_id: str, comment: str | None = None, mailbox: str | None = None):
377
+ return await reply_all_message(graph=graph, message_id=message_id, comment=comment, mailbox=mailbox)
378
+
379
+ @mcp.tool(name="forward_message", description=forward_message.__doc__ or "")
380
+ async def _forward(message_id: str, to: list[str], comment: str | None = None, mailbox: str | None = None):
381
+ return await forward_message(graph=graph, message_id=message_id, to=to, comment=comment, mailbox=mailbox)
382
+
383
+ @mcp.tool(name="update_message", description=update_message.__doc__ or "")
384
+ async def _update(
385
+ message_id: str,
386
+ is_read: bool | None = None,
387
+ flag: Literal["notFlagged", "flagged", "complete"] | None = None,
388
+ importance: Literal["low", "normal", "high"] | None = None,
389
+ categories: list[str] | None = None,
390
+ parent_folder_id: str | None = None,
391
+ mailbox: str | None = None,
392
+ include_raw: bool = False,
393
+ ):
394
+ return await update_message(
395
+ graph=graph, message_id=message_id,
396
+ is_read=is_read, flag=flag, importance=importance,
397
+ categories=categories, parent_folder_id=parent_folder_id,
398
+ mailbox=mailbox, include_raw=include_raw,
399
+ )
400
+
401
+ @mcp.tool(name="delete_message", description=delete_message.__doc__ or "")
402
+ async def _delete(message_id: str, mailbox: str | None = None):
403
+ return await delete_message(graph=graph, message_id=message_id, mailbox=mailbox)
@@ -0,0 +1,223 @@
1
+ """Teams channel read tools: joined teams, channels, channel messages, replies."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from kiota_abstractions.base_request_configuration import RequestConfiguration
6
+ from msgraph.generated.teams.item.channels.item.messages.messages_request_builder import (
7
+ MessagesRequestBuilder as ChannelMessagesRequestBuilder,
8
+ )
9
+ from msgraph.generated.teams.item.channels.item.messages.item.replies.replies_request_builder import (
10
+ RepliesRequestBuilder,
11
+ )
12
+ from msgraph_mcp.auth.token import NotAuthenticatedError
13
+ from msgraph_mcp.graph.errors import map_kiota_error
14
+ from msgraph_mcp.graph.pagination import decode_page_token, encode_next_link, validate_limit
15
+ from msgraph_mcp.graph.serialize import channel_to_dict, chat_message_to_dict, team_to_dict
16
+ from msgraph_mcp.graph.trimming import trim_channel, trim_chat_message, trim_team
17
+
18
+ _CHANNEL_MSG_MAX = 50
19
+
20
+
21
+ def _channel_messages_query(*, limit: int):
22
+ qp = ChannelMessagesRequestBuilder.MessagesRequestBuilderGetQueryParameters(top=limit)
23
+ return RequestConfiguration[
24
+ ChannelMessagesRequestBuilder.MessagesRequestBuilderGetQueryParameters
25
+ ](query_parameters=qp)
26
+
27
+
28
+ def _replies_query(*, limit: int):
29
+ qp = RepliesRequestBuilder.RepliesRequestBuilderGetQueryParameters(top=limit)
30
+ return RequestConfiguration[
31
+ RepliesRequestBuilder.RepliesRequestBuilderGetQueryParameters
32
+ ](query_parameters=qp)
33
+
34
+
35
+ async def _paged(builder, *, page_token, request_configuration):
36
+ if page_token is not None:
37
+ url = decode_page_token(page_token)
38
+ return await builder.with_url(url).get()
39
+ return await builder.get(request_configuration=request_configuration)
40
+
41
+
42
+ async def list_joined_teams(
43
+ *, graph, limit: int = 25, page_token: str | None = None, include_raw: bool = False
44
+ ) -> dict:
45
+ """List the teams the signed-in user is a member of.
46
+
47
+ Args:
48
+ limit: 1-100. Default 25. Applied client-side (see below); teams
49
+ beyond the limit are dropped.
50
+ page_token: Continuation token from a previous result.
51
+ include_raw: Include the raw Graph payload under "raw" on each item.
52
+
53
+ Returns:
54
+ {"items": [trimmed_team, ...], "next_page_token": str | None}
55
+ """
56
+ limit = validate_limit(limit)
57
+ builder = graph.raw.me.joined_teams
58
+ try:
59
+ # /me/joinedTeams rejects $top ("Query option 'Top' is not allowed"),
60
+ # so fetch unpaged and cap client-side.
61
+ collection = await _paged(builder, page_token=page_token, request_configuration=None)
62
+ except NotAuthenticatedError:
63
+ raise
64
+ except Exception as exc: # noqa: BLE001
65
+ raise map_kiota_error(exc) from exc
66
+ items = [trim_team(team_to_dict(t), include_raw=include_raw) for t in (collection.value or [])[:limit]]
67
+ return {"items": items, "next_page_token": encode_next_link(getattr(collection, "odata_next_link", None))}
68
+
69
+
70
+ async def list_channels(
71
+ *, graph, team_id: str, limit: int = 25, page_token: str | None = None, include_raw: bool = False
72
+ ) -> dict:
73
+ """List channels in a team.
74
+
75
+ Args:
76
+ team_id: Graph team id (from list_joined_teams).
77
+ limit: 1-100. Default 25. Applied client-side (see below); channels
78
+ beyond the limit are dropped.
79
+ page_token: Continuation token from a previous result.
80
+ include_raw: Include the raw Graph payload under "raw" on each item.
81
+
82
+ Returns:
83
+ {"items": [trimmed_channel, ...], "next_page_token": str | None}
84
+ """
85
+ limit = validate_limit(limit)
86
+ builder = graph.raw.teams.by_team_id(team_id).channels
87
+ try:
88
+ # /teams/{id}/channels rejects $top ("Query option 'Top' is not
89
+ # allowed"), so fetch unpaged and cap client-side.
90
+ collection = await _paged(builder, page_token=page_token, request_configuration=None)
91
+ except NotAuthenticatedError:
92
+ raise
93
+ except Exception as exc: # noqa: BLE001
94
+ raise map_kiota_error(exc) from exc
95
+ items = [trim_channel(channel_to_dict(c), include_raw=include_raw) for c in (collection.value or [])[:limit]]
96
+ return {"items": items, "next_page_token": encode_next_link(getattr(collection, "odata_next_link", None))}
97
+
98
+
99
+ async def list_channel_messages(
100
+ *,
101
+ graph,
102
+ team_id: str,
103
+ channel_id: str,
104
+ limit: int = 25,
105
+ page_token: str | None = None,
106
+ include_body: bool = False,
107
+ include_raw: bool = False,
108
+ ) -> dict:
109
+ """List root messages in a channel, newest first.
110
+
111
+ Replies are not included here; fetch them with list_message_replies.
112
+
113
+ Args:
114
+ team_id: Graph team id.
115
+ channel_id: Graph channel id (from list_channels).
116
+ limit: 1-50 (Graph caps channel message pages at 50). Default 25.
117
+ page_token: Continuation token from a previous result.
118
+ include_body: When True, include each message's full body and any
119
+ attachment card payloads (e.g. Adaptive Card JSON for bot posts).
120
+ Default False (snippet only; card text still feeds the snippet).
121
+ include_raw: Include the raw Graph payload under "raw" on each item.
122
+
123
+ Returns:
124
+ {"items": [trimmed_chat_message, ...], "next_page_token": str | None}
125
+ """
126
+ limit = validate_limit(limit, maximum=_CHANNEL_MSG_MAX)
127
+ builder = graph.raw.teams.by_team_id(team_id).channels.by_channel_id(channel_id).messages
128
+ try:
129
+ collection = await _paged(builder, page_token=page_token, request_configuration=_channel_messages_query(limit=limit))
130
+ except NotAuthenticatedError:
131
+ raise
132
+ except Exception as exc: # noqa: BLE001
133
+ raise map_kiota_error(exc) from exc
134
+ items = [
135
+ trim_chat_message(chat_message_to_dict(m), include_body=include_body, include_raw=include_raw)
136
+ for m in (collection.value or [])
137
+ ]
138
+ return {"items": items, "next_page_token": encode_next_link(getattr(collection, "odata_next_link", None))}
139
+
140
+
141
+ async def list_message_replies(
142
+ *,
143
+ graph,
144
+ team_id: str,
145
+ channel_id: str,
146
+ message_id: str,
147
+ limit: int = 25,
148
+ page_token: str | None = None,
149
+ include_body: bool = False,
150
+ include_raw: bool = False,
151
+ ) -> dict:
152
+ """List replies to a channel message (the thread under a root post), newest first.
153
+
154
+ Args:
155
+ team_id: Graph team id.
156
+ channel_id: Graph channel id.
157
+ message_id: Graph id of the root channel message (from list_channel_messages).
158
+ limit: 1-50 (Graph caps reply pages at 50). Default 25.
159
+ page_token: Continuation token from a previous result.
160
+ include_body: When True, include each reply's full body and any
161
+ attachment card payloads. Default False (snippet only).
162
+ include_raw: Include the raw Graph payload under "raw" on each item.
163
+
164
+ Returns:
165
+ {"items": [trimmed_chat_message, ...], "next_page_token": str | None}
166
+ """
167
+ limit = validate_limit(limit, maximum=_CHANNEL_MSG_MAX)
168
+ builder = (
169
+ graph.raw.teams.by_team_id(team_id)
170
+ .channels.by_channel_id(channel_id)
171
+ .messages.by_chat_message_id(message_id)
172
+ .replies
173
+ )
174
+ try:
175
+ collection = await _paged(builder, page_token=page_token, request_configuration=_replies_query(limit=limit))
176
+ except NotAuthenticatedError:
177
+ raise
178
+ except Exception as exc: # noqa: BLE001
179
+ raise map_kiota_error(exc) from exc
180
+ items = [
181
+ trim_chat_message(chat_message_to_dict(m), include_body=include_body, include_raw=include_raw)
182
+ for m in (collection.value or [])
183
+ ]
184
+ return {"items": items, "next_page_token": encode_next_link(getattr(collection, "odata_next_link", None))}
185
+
186
+
187
+ def register(mcp, *, graph) -> None:
188
+ @mcp.tool(name="list_joined_teams", description=list_joined_teams.__doc__ or "")
189
+ async def _list_joined_teams(limit: int = 25, page_token: str | None = None, include_raw: bool = False):
190
+ return await list_joined_teams(graph=graph, limit=limit, page_token=page_token, include_raw=include_raw)
191
+
192
+ @mcp.tool(name="list_channels", description=list_channels.__doc__ or "")
193
+ async def _list_channels(team_id: str, limit: int = 25, page_token: str | None = None, include_raw: bool = False):
194
+ return await list_channels(graph=graph, team_id=team_id, limit=limit, page_token=page_token, include_raw=include_raw)
195
+
196
+ @mcp.tool(name="list_channel_messages", description=list_channel_messages.__doc__ or "")
197
+ async def _list_channel_messages(
198
+ team_id: str,
199
+ channel_id: str,
200
+ limit: int = 25,
201
+ page_token: str | None = None,
202
+ include_body: bool = False,
203
+ include_raw: bool = False,
204
+ ):
205
+ return await list_channel_messages(
206
+ graph=graph, team_id=team_id, channel_id=channel_id, limit=limit,
207
+ page_token=page_token, include_body=include_body, include_raw=include_raw,
208
+ )
209
+
210
+ @mcp.tool(name="list_message_replies", description=list_message_replies.__doc__ or "")
211
+ async def _list_message_replies(
212
+ team_id: str,
213
+ channel_id: str,
214
+ message_id: str,
215
+ limit: int = 25,
216
+ page_token: str | None = None,
217
+ include_body: bool = False,
218
+ include_raw: bool = False,
219
+ ):
220
+ return await list_message_replies(
221
+ graph=graph, team_id=team_id, channel_id=channel_id, message_id=message_id,
222
+ limit=limit, page_token=page_token, include_body=include_body, include_raw=include_raw,
223
+ )