@mindstone/mcp-server-microsoft-teams 0.1.2 → 0.3.0
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.
- package/CHANGELOG.md +35 -1
- package/README.md +15 -5
- package/dist/resources/compose-message-template.d.ts +2 -0
- package/dist/resources/compose-message-template.js +968 -0
- package/dist/server.js +5 -1
- package/dist/teams.d.ts +10 -0
- package/dist/teams.js +374 -21
- package/dist/tools.d.ts +7 -0
- package/dist/tools.js +259 -5
- package/dist/untrusted-content.d.ts +28 -5
- package/dist/untrusted-content.js +62 -8
- package/dist/utils.js +9 -2
- package/package.json +5 -1
- package/server.json +2 -2
package/dist/server.js
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
2
|
import { SERVER_NAME, SERVER_VERSION } from './types.js';
|
|
3
|
-
import { registerTeamsTools } from './tools.js';
|
|
3
|
+
import { COMPOSE_MESSAGE_RESOURCE_URI, registerTeamsTools } from './tools.js';
|
|
4
|
+
import { COMPOSE_MESSAGE_HTML } from './resources/compose-message-template.js';
|
|
4
5
|
export function createServer() {
|
|
5
6
|
const server = new McpServer({
|
|
6
7
|
name: SERVER_NAME,
|
|
7
8
|
version: SERVER_VERSION,
|
|
8
9
|
});
|
|
9
10
|
registerTeamsTools(server);
|
|
11
|
+
server.registerResource('compose-message', COMPOSE_MESSAGE_RESOURCE_URI, { mimeType: 'text/html' }, async (uri) => ({
|
|
12
|
+
contents: [{ uri: uri.href, mimeType: 'text/html', text: COMPOSE_MESSAGE_HTML }],
|
|
13
|
+
}));
|
|
10
14
|
return server;
|
|
11
15
|
}
|
|
12
16
|
//# sourceMappingURL=server.js.map
|
package/dist/teams.d.ts
CHANGED
|
@@ -11,5 +11,15 @@ export declare function sendChatMessage(client: Client, args: ArgBag, signal: Ab
|
|
|
11
11
|
export declare function listTeams(client: Client, _args: ArgBag, signal: AbortSignal): Promise<unknown>;
|
|
12
12
|
export declare function listChannels(client: Client, args: ArgBag, signal: AbortSignal): Promise<unknown>;
|
|
13
13
|
export declare function getPresence(client: Client, _args: ArgBag, signal: AbortSignal): Promise<unknown>;
|
|
14
|
+
export declare function getUserPresence(client: Client, args: ArgBag, signal: AbortSignal): Promise<unknown>;
|
|
15
|
+
export declare const PRESENCE_AVAILABILITY_VALUES: readonly ["Available", "Busy", "DoNotDisturb", "BeRightBack", "Away", "Offline"];
|
|
16
|
+
export declare function setPresence(client: Client, args: ArgBag, signal: AbortSignal): Promise<unknown>;
|
|
17
|
+
export declare function listChannelMessages(client: Client, args: ArgBag, signal: AbortSignal): Promise<unknown>;
|
|
18
|
+
export declare function sendChannelMessage(client: Client, args: ArgBag, signal: AbortSignal): Promise<unknown>;
|
|
19
|
+
export declare function replyToChannelMessage(client: Client, args: ArgBag, signal: AbortSignal): Promise<unknown>;
|
|
20
|
+
export declare function replyToChatMessage(client: Client, args: ArgBag, signal: AbortSignal): Promise<unknown>;
|
|
21
|
+
export declare function searchMessages(client: Client, args: ArgBag, signal: AbortSignal): Promise<unknown>;
|
|
22
|
+
export declare function findUser(client: Client, args: ArgBag, signal: AbortSignal): Promise<unknown>;
|
|
23
|
+
export declare function createChat(client: Client, args: ArgBag, signal: AbortSignal): Promise<unknown>;
|
|
14
24
|
export {};
|
|
15
25
|
//# sourceMappingURL=teams.d.ts.map
|
package/dist/teams.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
1
2
|
import { wrapUntrusted } from './untrusted-content.js';
|
|
2
3
|
export class TeamsBusinessError extends Error {
|
|
3
4
|
nextStep;
|
|
@@ -28,6 +29,23 @@ function clampTop(value, defaultValue, maxValue) {
|
|
|
28
29
|
return defaultValue;
|
|
29
30
|
return Math.max(1, Math.min(value, maxValue));
|
|
30
31
|
}
|
|
32
|
+
// Structural Graph values (IDs, enum-like tokens, timestamps) are vendor-assigned
|
|
33
|
+
// rather than user-authored free text, so instead of an untrusted-content
|
|
34
|
+
// envelope — which would corrupt an ID the model must pass back verbatim in
|
|
35
|
+
// follow-up calls — they are validated against shapes that cannot carry
|
|
36
|
+
// envelope-breakout or markup characters. An unexpected value fails closed: the
|
|
37
|
+
// Zod error surfaces through the standard (enveloped) error path.
|
|
38
|
+
const graphStructuralTokenSchema = z
|
|
39
|
+
.string()
|
|
40
|
+
.max(512)
|
|
41
|
+
.regex(/^[^\s<>"'`\\]*$/, 'unexpected characters in Microsoft Graph identifier');
|
|
42
|
+
const graphIsoDateTimeSchema = z
|
|
43
|
+
.string()
|
|
44
|
+
.regex(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})?$/, 'unexpected Microsoft Graph datetime format');
|
|
45
|
+
const graphPresenceStateSchema = z
|
|
46
|
+
.string()
|
|
47
|
+
.max(64)
|
|
48
|
+
.regex(/^[A-Za-z][A-Za-z0-9]*$/, 'unexpected Microsoft Graph presence value');
|
|
31
49
|
const HTML_ENTITIES = {
|
|
32
50
|
' ': ' ',
|
|
33
51
|
'&': '&',
|
|
@@ -58,15 +76,15 @@ function requireStringArg(args, name, label, nextStep) {
|
|
|
58
76
|
return value;
|
|
59
77
|
throw new TeamsBusinessError(`Missing required parameter: "${name}" (${label}).`, nextStep);
|
|
60
78
|
}
|
|
61
|
-
function formatMessage(msg) {
|
|
79
|
+
function formatMessage(msg, tool) {
|
|
62
80
|
return {
|
|
63
|
-
id: msg.id,
|
|
81
|
+
id: graphStructuralTokenSchema.nullish().parse(msg.id),
|
|
64
82
|
from: msg.from?.user?.displayName
|
|
65
|
-
? wrapUntrusted(msg.from.user.displayName,
|
|
83
|
+
? wrapUntrusted(msg.from.user.displayName, `microsoft-teams:${tool}:from`)
|
|
66
84
|
: 'Unknown',
|
|
67
|
-
content: wrapUntrusted(stripHtml(msg.body?.content ?? ''),
|
|
68
|
-
contentType: msg.body?.contentType,
|
|
69
|
-
createdAt: msg.createdDateTime,
|
|
85
|
+
content: wrapUntrusted(stripHtml(msg.body?.content ?? ''), `microsoft-teams:${tool}:content`),
|
|
86
|
+
contentType: graphStructuralTokenSchema.nullish().parse(msg.body?.contentType),
|
|
87
|
+
createdAt: graphIsoDateTimeSchema.nullish().parse(msg.createdDateTime),
|
|
70
88
|
};
|
|
71
89
|
}
|
|
72
90
|
export async function listChats(client, args, signal) {
|
|
@@ -85,13 +103,13 @@ export async function listChats(client, args, signal) {
|
|
|
85
103
|
return {
|
|
86
104
|
count: chats.length,
|
|
87
105
|
chats: chats.map((chat) => ({
|
|
88
|
-
id: chat.id,
|
|
106
|
+
id: graphStructuralTokenSchema.parse(chat.id),
|
|
89
107
|
topic: chat.topic
|
|
90
108
|
? wrapUntrusted(chat.topic, 'microsoft-teams:list_chats:topic')
|
|
91
109
|
: '(No topic)',
|
|
92
|
-
type: chat.chatType,
|
|
93
|
-
createdAt: chat.createdDateTime,
|
|
94
|
-
lastUpdated: chat.lastUpdatedDateTime,
|
|
110
|
+
type: graphStructuralTokenSchema.nullish().parse(chat.chatType),
|
|
111
|
+
createdAt: graphIsoDateTimeSchema.nullish().parse(chat.createdDateTime),
|
|
112
|
+
lastUpdated: graphIsoDateTimeSchema.nullish().parse(chat.lastUpdatedDateTime),
|
|
95
113
|
})),
|
|
96
114
|
};
|
|
97
115
|
}
|
|
@@ -103,17 +121,17 @@ export async function getChat(client, args, signal) {
|
|
|
103
121
|
.expand('members')
|
|
104
122
|
.get());
|
|
105
123
|
return {
|
|
106
|
-
id: chat.id,
|
|
124
|
+
id: graphStructuralTokenSchema.parse(chat.id),
|
|
107
125
|
topic: chat.topic
|
|
108
126
|
? wrapUntrusted(chat.topic, 'microsoft-teams:get_chat:topic')
|
|
109
127
|
: '(No topic)',
|
|
110
|
-
type: chat.chatType,
|
|
111
|
-
createdAt: chat.createdDateTime,
|
|
112
|
-
lastUpdated: chat.lastUpdatedDateTime,
|
|
128
|
+
type: graphStructuralTokenSchema.nullish().parse(chat.chatType),
|
|
129
|
+
createdAt: graphIsoDateTimeSchema.nullish().parse(chat.createdDateTime),
|
|
130
|
+
lastUpdated: graphIsoDateTimeSchema.nullish().parse(chat.lastUpdatedDateTime),
|
|
113
131
|
members: chat.members?.map((member) => ({
|
|
114
132
|
displayName: wrapUntrusted(member.displayName, 'microsoft-teams:get_chat:members.displayName'),
|
|
115
133
|
email: wrapUntrusted(member.email, 'microsoft-teams:get_chat:members.email'),
|
|
116
|
-
roles: member.roles,
|
|
134
|
+
roles: z.array(graphStructuralTokenSchema).nullish().parse(member.roles),
|
|
117
135
|
})),
|
|
118
136
|
};
|
|
119
137
|
}
|
|
@@ -129,13 +147,13 @@ export async function listChatMessages(client, args, signal) {
|
|
|
129
147
|
return {
|
|
130
148
|
chatId,
|
|
131
149
|
count: messages.length,
|
|
132
|
-
messages: messages.map(formatMessage),
|
|
150
|
+
messages: messages.map((msg) => formatMessage(msg, 'list_chat_messages')),
|
|
133
151
|
};
|
|
134
152
|
}
|
|
135
153
|
export async function sendChatMessage(client, args, signal) {
|
|
136
154
|
const chatId = requireStringArg(args, 'chatId', 'chat ID', 'send_chat_message');
|
|
137
155
|
const content = requireStringArg(args, 'content', 'message body', 'send_chat_message');
|
|
138
|
-
const response = (await client.api(`/me/chats/${chatId}/messages`).options({ signal }).post({
|
|
156
|
+
const response = sendMessageResponseSchema.parse(await client.api(`/me/chats/${chatId}/messages`).options({ signal }).post({
|
|
139
157
|
body: {
|
|
140
158
|
contentType: content.includes('<') ? 'html' : 'text',
|
|
141
159
|
content,
|
|
@@ -149,10 +167,10 @@ export async function sendChatMessage(client, args, signal) {
|
|
|
149
167
|
}
|
|
150
168
|
function formatChannel(channel) {
|
|
151
169
|
return {
|
|
152
|
-
id: channel.id,
|
|
170
|
+
id: graphStructuralTokenSchema.parse(channel.id),
|
|
153
171
|
name: wrapUntrusted(channel.displayName, 'microsoft-teams:list_channels:name'),
|
|
154
172
|
description: wrapUntrusted(channel.description, 'microsoft-teams:list_channels:description'),
|
|
155
|
-
membershipType: channel.membershipType,
|
|
173
|
+
membershipType: graphStructuralTokenSchema.nullish().parse(channel.membershipType),
|
|
156
174
|
};
|
|
157
175
|
}
|
|
158
176
|
export async function listTeams(client, _args, signal) {
|
|
@@ -165,7 +183,7 @@ export async function listTeams(client, _args, signal) {
|
|
|
165
183
|
return {
|
|
166
184
|
count: teams.length,
|
|
167
185
|
teams: teams.map((team) => ({
|
|
168
|
-
id: team.id,
|
|
186
|
+
id: graphStructuralTokenSchema.parse(team.id),
|
|
169
187
|
name: wrapUntrusted(team.displayName, 'microsoft-teams:list_teams:name'),
|
|
170
188
|
description: wrapUntrusted(team.description, 'microsoft-teams:list_teams:description'),
|
|
171
189
|
})),
|
|
@@ -188,9 +206,344 @@ export async function listChannels(client, args, signal) {
|
|
|
188
206
|
export async function getPresence(client, _args, signal) {
|
|
189
207
|
const presence = (await client.api('/me/presence').options({ signal }).get());
|
|
190
208
|
return {
|
|
209
|
+
availability: graphPresenceStateSchema.nullish().parse(presence.availability),
|
|
210
|
+
activity: graphPresenceStateSchema.nullish().parse(presence.activity),
|
|
211
|
+
statusMessage: wrapUntrusted(presence.statusMessage?.message?.content ?? undefined, 'microsoft-teams:get_presence:statusMessage'),
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
const presenceSchema = z
|
|
215
|
+
.object({
|
|
216
|
+
availability: graphPresenceStateSchema.nullish(),
|
|
217
|
+
activity: graphPresenceStateSchema.nullish(),
|
|
218
|
+
statusMessage: z
|
|
219
|
+
.object({
|
|
220
|
+
message: z
|
|
221
|
+
.object({
|
|
222
|
+
content: z.string().nullish(),
|
|
223
|
+
})
|
|
224
|
+
.nullish(),
|
|
225
|
+
})
|
|
226
|
+
.nullish(),
|
|
227
|
+
})
|
|
228
|
+
.passthrough();
|
|
229
|
+
export async function getUserPresence(client, args, signal) {
|
|
230
|
+
const userId = requireStringArg(args, 'userId', 'user ID or email', 'get_user_presence');
|
|
231
|
+
const presence = presenceSchema.parse(await client.api(`/users/${encodeURIComponent(userId)}/presence`).options({ signal }).get());
|
|
232
|
+
return {
|
|
233
|
+
userId,
|
|
191
234
|
availability: presence.availability,
|
|
192
235
|
activity: presence.activity,
|
|
193
|
-
statusMessage: wrapUntrusted(presence.statusMessage?.message?.content ?? undefined, 'microsoft-teams:
|
|
236
|
+
statusMessage: wrapUntrusted(presence.statusMessage?.message?.content ?? undefined, 'microsoft-teams:get_user_presence:statusMessage'),
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
export const PRESENCE_AVAILABILITY_VALUES = [
|
|
240
|
+
'Available',
|
|
241
|
+
'Busy',
|
|
242
|
+
'DoNotDisturb',
|
|
243
|
+
'BeRightBack',
|
|
244
|
+
'Away',
|
|
245
|
+
'Offline',
|
|
246
|
+
];
|
|
247
|
+
export async function setPresence(client, args, signal) {
|
|
248
|
+
const availability = requireStringArg(args, 'availability', `presence availability (${PRESENCE_AVAILABILITY_VALUES.join(', ')})`, 'set_presence');
|
|
249
|
+
const duration = numberArg(args, 'durationMinutes');
|
|
250
|
+
// The tool schema bounds this to an integer in 5-480; keep the business layer
|
|
251
|
+
// fail-closed too rather than silently coercing a value into a different
|
|
252
|
+
// meaning before a network write.
|
|
253
|
+
if (duration != null && (!Number.isInteger(duration) || duration < 5 || duration > 480)) {
|
|
254
|
+
throw new TeamsBusinessError('Invalid "durationMinutes": must be a whole number of minutes between 5 and 480.', 'set_presence');
|
|
255
|
+
}
|
|
256
|
+
const durationMinutes = duration;
|
|
257
|
+
const body = { availability, activity: availability };
|
|
258
|
+
if (durationMinutes != null)
|
|
259
|
+
body.expirationDuration = `PT${durationMinutes}M`;
|
|
260
|
+
await client.api('/me/presence/setUserPreferredPresence').options({ signal }).post(body);
|
|
261
|
+
return {
|
|
262
|
+
success: true,
|
|
263
|
+
availability,
|
|
264
|
+
...(durationMinutes != null ? { durationMinutes } : {}),
|
|
265
|
+
message: `Presence set to ${availability}`,
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
// ---------------------------------------------------------------------------
|
|
269
|
+
// Channel messages
|
|
270
|
+
// ---------------------------------------------------------------------------
|
|
271
|
+
// Every function validates the Graph strings it returns — free text via the
|
|
272
|
+
// untrusted-content envelope, structural values (IDs, enum-like tokens,
|
|
273
|
+
// datetimes) via the shape schemas above — so vendor-controlled markup never
|
|
274
|
+
// reaches model-visible output raw. The older functions still cast at fetch
|
|
275
|
+
// time for typing, but their output mappers apply the same schemas.
|
|
276
|
+
const graphMessageSchema = z
|
|
277
|
+
.object({
|
|
278
|
+
id: graphStructuralTokenSchema.optional(),
|
|
279
|
+
replyToId: graphStructuralTokenSchema.nullish(),
|
|
280
|
+
from: z
|
|
281
|
+
.object({
|
|
282
|
+
user: z
|
|
283
|
+
.object({
|
|
284
|
+
id: graphStructuralTokenSchema.nullish(),
|
|
285
|
+
displayName: z.string().nullish(),
|
|
286
|
+
})
|
|
287
|
+
.nullish(),
|
|
288
|
+
})
|
|
289
|
+
.nullish(),
|
|
290
|
+
body: z
|
|
291
|
+
.object({
|
|
292
|
+
content: z.string().nullish(),
|
|
293
|
+
contentType: graphStructuralTokenSchema.nullish(),
|
|
294
|
+
})
|
|
295
|
+
.nullish(),
|
|
296
|
+
createdDateTime: graphIsoDateTimeSchema.nullish(),
|
|
297
|
+
})
|
|
298
|
+
.passthrough();
|
|
299
|
+
const graphMessageCollectionSchema = z
|
|
300
|
+
.object({
|
|
301
|
+
value: z.array(graphMessageSchema).nullish(),
|
|
302
|
+
'@odata.nextLink': z.string().nullish(),
|
|
303
|
+
})
|
|
304
|
+
.passthrough();
|
|
305
|
+
const sendMessageResponseSchema = z
|
|
306
|
+
.object({
|
|
307
|
+
id: graphStructuralTokenSchema.optional(),
|
|
308
|
+
})
|
|
309
|
+
.passthrough();
|
|
310
|
+
function messagePostBody(content) {
|
|
311
|
+
return {
|
|
312
|
+
body: {
|
|
313
|
+
contentType: content.includes('<') ? 'html' : 'text',
|
|
314
|
+
content,
|
|
315
|
+
},
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
export async function listChannelMessages(client, args, signal) {
|
|
319
|
+
const teamId = requireStringArg(args, 'teamId', 'team ID', 'list_channel_messages');
|
|
320
|
+
const channelId = requireStringArg(args, 'channelId', 'channel ID', 'list_channel_messages');
|
|
321
|
+
const top = clampTop(numberArg(args, 'top'), 25, 50);
|
|
322
|
+
const response = graphMessageCollectionSchema.parse(await client
|
|
323
|
+
.api(`/teams/${teamId}/channels/${channelId}/messages`)
|
|
324
|
+
.options({ signal })
|
|
325
|
+
.top(top)
|
|
326
|
+
.get());
|
|
327
|
+
const messages = response.value ?? [];
|
|
328
|
+
return {
|
|
329
|
+
teamId,
|
|
330
|
+
channelId,
|
|
331
|
+
count: messages.length,
|
|
332
|
+
hasMore: Boolean(response['@odata.nextLink']),
|
|
333
|
+
messages: messages.map((msg) => ({
|
|
334
|
+
...formatMessage(msg, 'list_channel_messages'),
|
|
335
|
+
replyToId: msg.replyToId ?? undefined,
|
|
336
|
+
})),
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
export async function sendChannelMessage(client, args, signal) {
|
|
340
|
+
const teamId = requireStringArg(args, 'teamId', 'team ID', 'send_channel_message');
|
|
341
|
+
const channelId = requireStringArg(args, 'channelId', 'channel ID', 'send_channel_message');
|
|
342
|
+
const content = requireStringArg(args, 'content', 'message body', 'send_channel_message');
|
|
343
|
+
const response = sendMessageResponseSchema.parse(await client
|
|
344
|
+
.api(`/teams/${teamId}/channels/${channelId}/messages`)
|
|
345
|
+
.options({ signal })
|
|
346
|
+
.post(messagePostBody(content)));
|
|
347
|
+
return {
|
|
348
|
+
success: true,
|
|
349
|
+
messageId: response.id,
|
|
350
|
+
message: 'Message sent successfully',
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
export async function replyToChannelMessage(client, args, signal) {
|
|
354
|
+
const teamId = requireStringArg(args, 'teamId', 'team ID', 'reply_to_channel_message');
|
|
355
|
+
const channelId = requireStringArg(args, 'channelId', 'channel ID', 'reply_to_channel_message');
|
|
356
|
+
const messageId = requireStringArg(args, 'messageId', 'channel message ID', 'reply_to_channel_message');
|
|
357
|
+
const content = requireStringArg(args, 'content', 'reply body', 'reply_to_channel_message');
|
|
358
|
+
const response = sendMessageResponseSchema.parse(await client
|
|
359
|
+
.api(`/teams/${teamId}/channels/${channelId}/messages/${messageId}/replies`)
|
|
360
|
+
.options({ signal })
|
|
361
|
+
.post(messagePostBody(content)));
|
|
362
|
+
return {
|
|
363
|
+
success: true,
|
|
364
|
+
messageId: response.id,
|
|
365
|
+
message: 'Reply sent successfully',
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
export async function replyToChatMessage(client, args, signal) {
|
|
369
|
+
const chatId = requireStringArg(args, 'chatId', 'chat ID', 'reply_to_message');
|
|
370
|
+
const messageId = requireStringArg(args, 'messageId', 'message ID', 'reply_to_message');
|
|
371
|
+
const content = requireStringArg(args, 'content', 'reply body', 'reply_to_message');
|
|
372
|
+
const response = sendMessageResponseSchema.parse(await client
|
|
373
|
+
.api(`/me/chats/${chatId}/messages/${messageId}/replies`)
|
|
374
|
+
.options({ signal })
|
|
375
|
+
.post(messagePostBody(content)));
|
|
376
|
+
return {
|
|
377
|
+
success: true,
|
|
378
|
+
messageId: response.id,
|
|
379
|
+
message: 'Reply sent successfully',
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
// ---------------------------------------------------------------------------
|
|
383
|
+
// Message search
|
|
384
|
+
// ---------------------------------------------------------------------------
|
|
385
|
+
const searchHitSchema = z
|
|
386
|
+
.object({
|
|
387
|
+
hitId: z.string().nullish(),
|
|
388
|
+
summary: z.string().nullish(),
|
|
389
|
+
resource: graphMessageSchema.extend({ chatId: graphStructuralTokenSchema.nullish() }).nullish(),
|
|
390
|
+
})
|
|
391
|
+
.passthrough();
|
|
392
|
+
const searchResponseSchema = z
|
|
393
|
+
.object({
|
|
394
|
+
value: z
|
|
395
|
+
.array(z
|
|
396
|
+
.object({
|
|
397
|
+
hitsContainers: z
|
|
398
|
+
.array(z
|
|
399
|
+
.object({
|
|
400
|
+
hits: z.array(searchHitSchema).nullish(),
|
|
401
|
+
total: z.number().nullish(),
|
|
402
|
+
})
|
|
403
|
+
.passthrough())
|
|
404
|
+
.nullish(),
|
|
405
|
+
})
|
|
406
|
+
.passthrough())
|
|
407
|
+
.nullish(),
|
|
408
|
+
})
|
|
409
|
+
.passthrough();
|
|
410
|
+
export async function searchMessages(client, args, signal) {
|
|
411
|
+
const query = requireStringArg(args, 'query', 'search text', 'search_messages');
|
|
412
|
+
const top = clampTop(numberArg(args, 'top'), 10, 25);
|
|
413
|
+
const response = searchResponseSchema.parse(await client
|
|
414
|
+
.api('/search/query')
|
|
415
|
+
.options({ signal })
|
|
416
|
+
.post({
|
|
417
|
+
requests: [
|
|
418
|
+
{
|
|
419
|
+
entityTypes: ['chatMessage'],
|
|
420
|
+
query: { queryString: query },
|
|
421
|
+
from: 0,
|
|
422
|
+
size: top,
|
|
423
|
+
},
|
|
424
|
+
],
|
|
425
|
+
}));
|
|
426
|
+
const container = response.value?.[0]?.hitsContainers?.[0];
|
|
427
|
+
const hits = container?.hits ?? [];
|
|
428
|
+
return {
|
|
429
|
+
query,
|
|
430
|
+
count: hits.length,
|
|
431
|
+
total: container?.total ?? hits.length,
|
|
432
|
+
results: hits.map((hit) => ({
|
|
433
|
+
...formatMessage(hit.resource ?? {}, 'search_messages'),
|
|
434
|
+
chatId: hit.resource?.chatId ?? undefined,
|
|
435
|
+
summary: wrapUntrusted(hit.summary ? stripHtml(hit.summary) : undefined, 'microsoft-teams:search_messages:summary'),
|
|
436
|
+
})),
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
// ---------------------------------------------------------------------------
|
|
440
|
+
// User lookup and chat creation
|
|
441
|
+
// ---------------------------------------------------------------------------
|
|
442
|
+
const graphUserSchema = z
|
|
443
|
+
.object({
|
|
444
|
+
id: graphStructuralTokenSchema,
|
|
445
|
+
displayName: z.string().nullish(),
|
|
446
|
+
mail: z.string().nullish(),
|
|
447
|
+
userPrincipalName: z.string().nullish(),
|
|
448
|
+
})
|
|
449
|
+
.passthrough();
|
|
450
|
+
const graphUserCollectionSchema = z
|
|
451
|
+
.object({
|
|
452
|
+
value: z.array(graphUserSchema).nullish(),
|
|
453
|
+
'@odata.nextLink': z.string().nullish(),
|
|
454
|
+
})
|
|
455
|
+
.passthrough();
|
|
456
|
+
function formatUser(user, tool) {
|
|
457
|
+
return {
|
|
458
|
+
id: user.id,
|
|
459
|
+
displayName: wrapUntrusted(user.displayName ?? undefined, `microsoft-teams:${tool}:displayName`),
|
|
460
|
+
email: wrapUntrusted(user.mail ?? user.userPrincipalName ?? undefined, `microsoft-teams:${tool}:email`),
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
export async function findUser(client, args, signal) {
|
|
464
|
+
const query = requireStringArg(args, 'query', 'name or email address', 'find_user');
|
|
465
|
+
if (query.includes('@')) {
|
|
466
|
+
try {
|
|
467
|
+
const user = graphUserSchema.parse(await client
|
|
468
|
+
.api(`/users/${encodeURIComponent(query)}`)
|
|
469
|
+
.options({ signal })
|
|
470
|
+
.select('id,displayName,mail,userPrincipalName')
|
|
471
|
+
.get());
|
|
472
|
+
return { count: 1, hasMore: false, users: [formatUser(user, 'find_user')] };
|
|
473
|
+
}
|
|
474
|
+
catch (err) {
|
|
475
|
+
// A 404 here means "no such user", which is a result, not a failure.
|
|
476
|
+
if (err?.statusCode === 404) {
|
|
477
|
+
return { count: 0, hasMore: false, users: [] };
|
|
478
|
+
}
|
|
479
|
+
throw err;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
// $search on /users requires the ConsistencyLevel: eventual header; strip
|
|
483
|
+
// characters that would break out of the quoted search expression.
|
|
484
|
+
const safeQuery = query.replace(/["\\]/g, ' ').trim();
|
|
485
|
+
const response = graphUserCollectionSchema.parse(await client
|
|
486
|
+
.api('/users')
|
|
487
|
+
.options({ signal })
|
|
488
|
+
.header('ConsistencyLevel', 'eventual')
|
|
489
|
+
.search(`"displayName:${safeQuery}"`)
|
|
490
|
+
.select('id,displayName,mail,userPrincipalName')
|
|
491
|
+
.top(10)
|
|
492
|
+
.get());
|
|
493
|
+
const users = response.value ?? [];
|
|
494
|
+
// Name searches are capped at 10 results; surface Graph's continuation link
|
|
495
|
+
// so a truncated result is never mistaken for a complete one.
|
|
496
|
+
return {
|
|
497
|
+
count: users.length,
|
|
498
|
+
hasMore: Boolean(response['@odata.nextLink']),
|
|
499
|
+
users: users.map((user) => formatUser(user, 'find_user')),
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
function requireStringArrayArg(args, name, label, nextStep) {
|
|
503
|
+
const value = args[name];
|
|
504
|
+
if (Array.isArray(value) && value.length > 0) {
|
|
505
|
+
const members = value.map((entry) => (typeof entry === 'string' ? entry.trim() : ''));
|
|
506
|
+
if (members.every((entry) => entry.length > 0))
|
|
507
|
+
return members;
|
|
508
|
+
}
|
|
509
|
+
throw new TeamsBusinessError(`Missing required parameter: "${name}" (${label}). Provide a non-empty array of email addresses or user IDs.`, nextStep);
|
|
510
|
+
}
|
|
511
|
+
const createdChatSchema = z
|
|
512
|
+
.object({
|
|
513
|
+
id: graphStructuralTokenSchema,
|
|
514
|
+
chatType: z.enum(['oneOnOne', 'group', 'meeting']).nullish(),
|
|
515
|
+
})
|
|
516
|
+
.passthrough();
|
|
517
|
+
export async function createChat(client, args, signal) {
|
|
518
|
+
const members = [...new Set(requireStringArrayArg(args, 'members', 'chat participants', 'create_chat'))];
|
|
519
|
+
const topic = stringArg(args, 'topic');
|
|
520
|
+
const chatType = members.length === 1 ? 'oneOnOne' : 'group';
|
|
521
|
+
const memberBindings = [
|
|
522
|
+
{
|
|
523
|
+
'@odata.type': '#microsoft.graph.aadUserConversationMember',
|
|
524
|
+
roles: ['owner'],
|
|
525
|
+
'user@odata.bind': 'https://graph.microsoft.com/v1.0/me',
|
|
526
|
+
},
|
|
527
|
+
...members.map((member) => ({
|
|
528
|
+
'@odata.type': '#microsoft.graph.aadUserConversationMember',
|
|
529
|
+
roles: ['owner'],
|
|
530
|
+
'user@odata.bind': `https://graph.microsoft.com/v1.0/users/${encodeURIComponent(member)}`,
|
|
531
|
+
})),
|
|
532
|
+
];
|
|
533
|
+
const chat = createdChatSchema.parse(await client
|
|
534
|
+
.api('/chats')
|
|
535
|
+
.options({ signal })
|
|
536
|
+
.post({
|
|
537
|
+
chatType,
|
|
538
|
+
// Topic is only valid on group chats.
|
|
539
|
+
...(chatType === 'group' && topic ? { topic } : {}),
|
|
540
|
+
'members@odata.bind': memberBindings,
|
|
541
|
+
}));
|
|
542
|
+
return {
|
|
543
|
+
success: true,
|
|
544
|
+
chatId: chat.id,
|
|
545
|
+
chatType: chat.chatType ?? chatType,
|
|
546
|
+
message: 'Chat created successfully',
|
|
194
547
|
};
|
|
195
548
|
}
|
|
196
549
|
//# sourceMappingURL=teams.js.map
|
package/dist/tools.d.ts
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
1
|
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
/**
|
|
3
|
+
* `ui://` resource URI the compose_chat_message iframe is served under. The
|
|
4
|
+
* shared compose-app template posts `send_chat_message` when the user clicks
|
|
5
|
+
* Send; the HTML twin lives in src/resources/compose-message-template.ts
|
|
6
|
+
* (generated from the shared compose-app package, drift-gated).
|
|
7
|
+
*/
|
|
8
|
+
export declare const COMPOSE_MESSAGE_RESOURCE_URI = "ui://microsoft-teams/compose-message";
|
|
2
9
|
export declare function registerTeamsTools(server: McpServer): void;
|
|
3
10
|
//# sourceMappingURL=tools.d.ts.map
|