@teamnetwork/m365-mcp-server 1.0.1 → 1.0.3
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/README.md +3 -1
- package/dist/graph/attachments.js +71 -0
- package/dist/graph/calendar.js +1 -1
- package/dist/graph/email.js +15 -8
- package/dist/tools/calendar/listEvents.js +13 -1
- package/dist/tools/email/downloadAttachment.js +69 -0
- package/dist/tools/email/getMessage.js +2 -2
- package/dist/tools/email/listMessageAttachments.js +30 -0
- package/dist/tools/email/listMessages.js +13 -1
- package/dist/tools/registry.js +4 -0
- package/dist/tools/schemas.js +10 -0
- package/dist/utils/selectFields.js +83 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -119,10 +119,12 @@ If installed globally, you can use the `m365-mcp` command instead:
|
|
|
119
119
|
| `list_mailboxes` | List configured accounts and mailboxes with read/write status |
|
|
120
120
|
| `list_folders` | List mail folders (top-level or child folders) |
|
|
121
121
|
| `list_messages` | List messages with optional filters: date range, subject, body keyword, sender |
|
|
122
|
-
| `get_message` | Get full message details including body |
|
|
122
|
+
| `get_message` | Get full message details including body. Set `includeAttachments: true` to expand attachment metadata inline |
|
|
123
123
|
| `send_message` | Send an email (non-readonly mailboxes only) |
|
|
124
124
|
| `reply_message` | Reply or reply-all to a message (non-readonly mailboxes only) |
|
|
125
125
|
| `move_message` | Move a message to another folder (non-readonly mailboxes only) |
|
|
126
|
+
| `list_message_attachments` | List all attachments on a message (file, item, and reference types) |
|
|
127
|
+
| `download_attachment` | Download a file attachment as base64. Images returned as MCP image content; reference attachments return their preview URL |
|
|
126
128
|
|
|
127
129
|
### Calendar
|
|
128
130
|
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { mapGraphError } from '../utils/errors.js';
|
|
2
|
+
// ── Helpers ────────────────────────────────────────────────────────────────
|
|
3
|
+
function deriveAttachmentType(odataType) {
|
|
4
|
+
if (odataType?.includes('referenceAttachment'))
|
|
5
|
+
return 'referenceAttachment';
|
|
6
|
+
if (odataType?.includes('itemAttachment'))
|
|
7
|
+
return 'itemAttachment';
|
|
8
|
+
return 'fileAttachment';
|
|
9
|
+
}
|
|
10
|
+
function normaliseAttachment(raw) {
|
|
11
|
+
const odataType = raw['@odata.type'];
|
|
12
|
+
const attachmentType = deriveAttachmentType(odataType);
|
|
13
|
+
const meta = {
|
|
14
|
+
id: raw['id'],
|
|
15
|
+
name: raw['name'],
|
|
16
|
+
contentType: raw['contentType'] ?? 'application/octet-stream',
|
|
17
|
+
size: raw['size'] ?? 0,
|
|
18
|
+
isInline: raw['isInline'] ?? false,
|
|
19
|
+
attachmentType,
|
|
20
|
+
};
|
|
21
|
+
if (attachmentType === 'referenceAttachment' && raw['previewUrl']) {
|
|
22
|
+
meta.previewUrl = raw['previewUrl'];
|
|
23
|
+
}
|
|
24
|
+
return meta;
|
|
25
|
+
}
|
|
26
|
+
// ── API calls ──────────────────────────────────────────────────────────────
|
|
27
|
+
export async function listAttachments(client, email, messageId) {
|
|
28
|
+
try {
|
|
29
|
+
const response = await client
|
|
30
|
+
.api(`/users/${email}/messages/${messageId}/attachments`)
|
|
31
|
+
.select('id,name,contentType,size,isInline,previewUrl')
|
|
32
|
+
.get();
|
|
33
|
+
return (response.value ?? []).map(normaliseAttachment);
|
|
34
|
+
}
|
|
35
|
+
catch (err) {
|
|
36
|
+
throw mapGraphError(err, 'list_message_attachments');
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
export async function downloadAttachment(client, email, messageId, attachmentId) {
|
|
40
|
+
try {
|
|
41
|
+
// Fetch the full attachment record including contentBytes for file attachments.
|
|
42
|
+
// contentBytes is already base64-encoded by the Graph API.
|
|
43
|
+
const raw = await client
|
|
44
|
+
.api(`/users/${email}/messages/${messageId}/attachments/${attachmentId}`)
|
|
45
|
+
.select('id,name,contentType,isInline,contentBytes,previewUrl,@odata.type')
|
|
46
|
+
.get();
|
|
47
|
+
const attachmentType = deriveAttachmentType(raw['@odata.type']);
|
|
48
|
+
if (attachmentType === 'referenceAttachment') {
|
|
49
|
+
return {
|
|
50
|
+
kind: 'reference',
|
|
51
|
+
previewUrl: raw['previewUrl'] ?? raw['name'],
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
if (attachmentType === 'itemAttachment') {
|
|
55
|
+
return {
|
|
56
|
+
kind: 'item',
|
|
57
|
+
name: raw['name'],
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
// fileAttachment — contentBytes is already base64 from Graph
|
|
61
|
+
return {
|
|
62
|
+
kind: 'file',
|
|
63
|
+
name: raw['name'],
|
|
64
|
+
mimeType: raw['contentType'] ?? 'application/octet-stream',
|
|
65
|
+
contentBytes: raw['contentBytes'],
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
catch (err) {
|
|
69
|
+
throw mapGraphError(err, 'download_attachment');
|
|
70
|
+
}
|
|
71
|
+
}
|
package/dist/graph/calendar.js
CHANGED
|
@@ -22,7 +22,7 @@ export async function listEvents(client, email, params) {
|
|
|
22
22
|
startDateTime: params.startDate,
|
|
23
23
|
endDateTime: params.endDate,
|
|
24
24
|
})
|
|
25
|
-
.select(
|
|
25
|
+
.select(params.select)
|
|
26
26
|
.top(params.maxResults)
|
|
27
27
|
.orderby('start/dateTime')
|
|
28
28
|
.get();
|
package/dist/graph/email.js
CHANGED
|
@@ -23,7 +23,7 @@ export async function listMessages(client, email, params) {
|
|
|
23
23
|
const folderId = params.folderId === 'Inbox' ? 'Inbox' : params.folderId;
|
|
24
24
|
let req = client
|
|
25
25
|
.api(`/users/${email}/mailFolders/${folderId}/messages`)
|
|
26
|
-
.select(
|
|
26
|
+
.select(params.select)
|
|
27
27
|
.top(params.maxResults)
|
|
28
28
|
.orderby('receivedDateTime desc');
|
|
29
29
|
if (params.bodySearch) {
|
|
@@ -69,15 +69,22 @@ export async function listMessages(client, email, params) {
|
|
|
69
69
|
throw mapGraphError(err, 'list_messages');
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
|
-
export async function getMessage(client, email, messageId, includeBody) {
|
|
72
|
+
export async function getMessage(client, email, messageId, includeBody, includeAttachments) {
|
|
73
73
|
try {
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
74
|
+
const selectFields = [
|
|
75
|
+
'id', 'subject', 'from', 'toRecipients', 'ccRecipients',
|
|
76
|
+
'receivedDateTime', 'hasAttachments', 'isRead',
|
|
77
|
+
];
|
|
78
|
+
if (includeBody)
|
|
79
|
+
selectFields.push('body');
|
|
80
|
+
let req = client
|
|
78
81
|
.api(`/users/${email}/messages/${messageId}`)
|
|
79
|
-
.select(
|
|
80
|
-
|
|
82
|
+
.select(selectFields.join(','));
|
|
83
|
+
if (includeAttachments) {
|
|
84
|
+
// Expand attachments inline — avoids a second round-trip for the caller
|
|
85
|
+
req = req.expand('attachments($select=id,name,contentType,size,isInline)');
|
|
86
|
+
}
|
|
87
|
+
return await req.get();
|
|
81
88
|
}
|
|
82
89
|
catch (err) {
|
|
83
90
|
throw mapGraphError(err, 'get_message');
|
|
@@ -2,19 +2,31 @@ import { ListEventsSchema } from '../schemas.js';
|
|
|
2
2
|
import { createGraphClient } from '../../graph/clientFactory.js';
|
|
3
3
|
import { listEvents } from '../../graph/calendar.js';
|
|
4
4
|
import { assertMailboxConfigured } from '../../middleware/readonlyGuard.js';
|
|
5
|
-
import { errorResponse } from '../../utils/errors.js';
|
|
5
|
+
import { errorResponse, ToolError } from '../../utils/errors.js';
|
|
6
6
|
import { logger } from '../../utils/logger.js';
|
|
7
|
+
import { resolveSelect, SelectValidationError, EVENT_SELECT_FIELDS, EVENT_SELECT_DEFAULT } from '../../utils/selectFields.js';
|
|
7
8
|
export function registerListEvents(server, config, auth) {
|
|
8
9
|
server.tool('list_events', 'List calendar events within a date range. Recurring events are expanded into individual instances.', ListEventsSchema.shape, async (params) => {
|
|
9
10
|
logger.info('list_events', { accountId: params.accountId, mailbox: params.mailbox });
|
|
10
11
|
try {
|
|
11
12
|
assertMailboxConfigured(config, params.accountId, params.mailbox);
|
|
13
|
+
let select;
|
|
14
|
+
try {
|
|
15
|
+
select = resolveSelect(params.select, EVENT_SELECT_FIELDS, EVENT_SELECT_DEFAULT);
|
|
16
|
+
}
|
|
17
|
+
catch (err) {
|
|
18
|
+
if (err instanceof SelectValidationError) {
|
|
19
|
+
return errorResponse(new ToolError(err.message, 'INVALID_SELECT_FIELD'));
|
|
20
|
+
}
|
|
21
|
+
throw err;
|
|
22
|
+
}
|
|
12
23
|
const token = await auth.getAccessToken(params.accountId);
|
|
13
24
|
const client = createGraphClient(token);
|
|
14
25
|
const result = await listEvents(client, params.mailbox, {
|
|
15
26
|
startDate: params.startDate,
|
|
16
27
|
endDate: params.endDate,
|
|
17
28
|
maxResults: params.maxResults,
|
|
29
|
+
select,
|
|
18
30
|
...(params.calendarId !== undefined && { calendarId: params.calendarId }),
|
|
19
31
|
});
|
|
20
32
|
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { DownloadAttachmentSchema } from '../schemas.js';
|
|
2
|
+
import { createGraphClient } from '../../graph/clientFactory.js';
|
|
3
|
+
import { downloadAttachment } from '../../graph/attachments.js';
|
|
4
|
+
import { assertMailboxConfigured } from '../../middleware/readonlyGuard.js';
|
|
5
|
+
import { errorResponse } from '../../utils/errors.js';
|
|
6
|
+
import { logger } from '../../utils/logger.js';
|
|
7
|
+
export function registerDownloadAttachment(server, config, auth) {
|
|
8
|
+
server.tool('download_attachment', 'Download a file attachment from an email message. Returns base64-encoded file content for file attachments, the preview URL for reference attachments (e.g. SharePoint links), or a descriptive message for embedded item attachments.', DownloadAttachmentSchema.shape, async (params) => {
|
|
9
|
+
logger.info('download_attachment', {
|
|
10
|
+
accountId: params.accountId,
|
|
11
|
+
mailbox: params.mailbox,
|
|
12
|
+
});
|
|
13
|
+
try {
|
|
14
|
+
assertMailboxConfigured(config, params.accountId, params.mailbox);
|
|
15
|
+
const token = await auth.getAccessToken(params.accountId);
|
|
16
|
+
const client = createGraphClient(token);
|
|
17
|
+
const result = await downloadAttachment(client, params.mailbox, params.messageId, params.attachmentId);
|
|
18
|
+
switch (result.kind) {
|
|
19
|
+
case 'file':
|
|
20
|
+
// Images are returned as MCP image content; all other files as structured text
|
|
21
|
+
// containing the base64 payload, mimeType, and fileName for the caller to handle.
|
|
22
|
+
if (result.mimeType.startsWith('image/')) {
|
|
23
|
+
return {
|
|
24
|
+
content: [
|
|
25
|
+
{
|
|
26
|
+
type: 'image',
|
|
27
|
+
data: result.contentBytes,
|
|
28
|
+
mimeType: result.mimeType,
|
|
29
|
+
},
|
|
30
|
+
],
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
content: [
|
|
35
|
+
{
|
|
36
|
+
type: 'text',
|
|
37
|
+
text: JSON.stringify({
|
|
38
|
+
fileName: result.name,
|
|
39
|
+
mimeType: result.mimeType,
|
|
40
|
+
encoding: 'base64',
|
|
41
|
+
data: result.contentBytes,
|
|
42
|
+
}, null, 2),
|
|
43
|
+
},
|
|
44
|
+
],
|
|
45
|
+
};
|
|
46
|
+
case 'reference':
|
|
47
|
+
return {
|
|
48
|
+
content: [{ type: 'text', text: result.previewUrl }],
|
|
49
|
+
};
|
|
50
|
+
case 'item':
|
|
51
|
+
return {
|
|
52
|
+
content: [
|
|
53
|
+
{
|
|
54
|
+
type: 'text',
|
|
55
|
+
text: `itemAttachment: "${result.name}" — embedded Outlook item, not downloadable as a file`,
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
logger.error('download_attachment failed', {
|
|
63
|
+
accountId: params.accountId,
|
|
64
|
+
mailbox: params.mailbox,
|
|
65
|
+
});
|
|
66
|
+
return errorResponse(err);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
}
|
|
@@ -5,13 +5,13 @@ import { assertMailboxConfigured } from '../../middleware/readonlyGuard.js';
|
|
|
5
5
|
import { errorResponse } from '../../utils/errors.js';
|
|
6
6
|
import { logger } from '../../utils/logger.js';
|
|
7
7
|
export function registerGetMessage(server, config, auth) {
|
|
8
|
-
server.tool('get_message', 'Get the full details of a specific email message by ID
|
|
8
|
+
server.tool('get_message', 'Get the full details of a specific email message by ID. Set includeBody to receive the message body, includeAttachments to expand attachment metadata inline.', GetMessageSchema.shape, async (params) => {
|
|
9
9
|
logger.info('get_message', { accountId: params.accountId, mailbox: params.mailbox });
|
|
10
10
|
try {
|
|
11
11
|
assertMailboxConfigured(config, params.accountId, params.mailbox);
|
|
12
12
|
const token = await auth.getAccessToken(params.accountId);
|
|
13
13
|
const client = createGraphClient(token);
|
|
14
|
-
const result = await getMessage(client, params.mailbox, params.messageId, params.includeBody);
|
|
14
|
+
const result = await getMessage(client, params.mailbox, params.messageId, params.includeBody, params.includeAttachments);
|
|
15
15
|
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
16
16
|
}
|
|
17
17
|
catch (err) {
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { ListMessageAttachmentsSchema } from '../schemas.js';
|
|
2
|
+
import { createGraphClient } from '../../graph/clientFactory.js';
|
|
3
|
+
import { listAttachments } from '../../graph/attachments.js';
|
|
4
|
+
import { assertMailboxConfigured } from '../../middleware/readonlyGuard.js';
|
|
5
|
+
import { errorResponse } from '../../utils/errors.js';
|
|
6
|
+
import { logger } from '../../utils/logger.js';
|
|
7
|
+
export function registerListMessageAttachments(server, config, auth) {
|
|
8
|
+
server.tool('list_message_attachments', 'List all attachments on a specific email message. Returns metadata for file, item, and reference attachments. Use download_attachment to retrieve file content.', ListMessageAttachmentsSchema.shape, async (params) => {
|
|
9
|
+
logger.info('list_message_attachments', {
|
|
10
|
+
accountId: params.accountId,
|
|
11
|
+
mailbox: params.mailbox,
|
|
12
|
+
});
|
|
13
|
+
try {
|
|
14
|
+
assertMailboxConfigured(config, params.accountId, params.mailbox);
|
|
15
|
+
const token = await auth.getAccessToken(params.accountId);
|
|
16
|
+
const client = createGraphClient(token);
|
|
17
|
+
const attachments = await listAttachments(client, params.mailbox, params.messageId);
|
|
18
|
+
return {
|
|
19
|
+
content: [{ type: 'text', text: JSON.stringify({ value: attachments }, null, 2) }],
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
catch (err) {
|
|
23
|
+
logger.error('list_message_attachments failed', {
|
|
24
|
+
accountId: params.accountId,
|
|
25
|
+
mailbox: params.mailbox,
|
|
26
|
+
});
|
|
27
|
+
return errorResponse(err);
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
}
|
|
@@ -2,8 +2,9 @@ import { ListMessagesSchema } from '../schemas.js';
|
|
|
2
2
|
import { createGraphClient } from '../../graph/clientFactory.js';
|
|
3
3
|
import { listMessages } from '../../graph/email.js';
|
|
4
4
|
import { assertMailboxConfigured } from '../../middleware/readonlyGuard.js';
|
|
5
|
-
import { errorResponse } from '../../utils/errors.js';
|
|
5
|
+
import { errorResponse, ToolError } from '../../utils/errors.js';
|
|
6
6
|
import { logger } from '../../utils/logger.js';
|
|
7
|
+
import { resolveSelect, SelectValidationError, MESSAGE_SELECT_FIELDS, MESSAGE_SELECT_DEFAULT } from '../../utils/selectFields.js';
|
|
7
8
|
export function registerListMessages(server, config, auth) {
|
|
8
9
|
server.tool('list_messages', 'List email messages in a mailbox folder with optional filters for date range, subject, body content, and sender. Returns message metadata (not full body) to keep response size small.', ListMessagesSchema.shape, async (params) => {
|
|
9
10
|
logger.info('list_messages', {
|
|
@@ -14,11 +15,22 @@ export function registerListMessages(server, config, auth) {
|
|
|
14
15
|
});
|
|
15
16
|
try {
|
|
16
17
|
assertMailboxConfigured(config, params.accountId, params.mailbox);
|
|
18
|
+
let select;
|
|
19
|
+
try {
|
|
20
|
+
select = resolveSelect(params.select, MESSAGE_SELECT_FIELDS, MESSAGE_SELECT_DEFAULT);
|
|
21
|
+
}
|
|
22
|
+
catch (err) {
|
|
23
|
+
if (err instanceof SelectValidationError) {
|
|
24
|
+
return errorResponse(new ToolError(err.message, 'INVALID_SELECT_FIELD'));
|
|
25
|
+
}
|
|
26
|
+
throw err;
|
|
27
|
+
}
|
|
17
28
|
const token = await auth.getAccessToken(params.accountId);
|
|
18
29
|
const client = createGraphClient(token);
|
|
19
30
|
const result = await listMessages(client, params.mailbox, {
|
|
20
31
|
folderId: params.folderId,
|
|
21
32
|
maxResults: params.maxResults,
|
|
33
|
+
select,
|
|
22
34
|
...(params.startDate !== undefined && { startDate: params.startDate }),
|
|
23
35
|
...(params.endDate !== undefined && { endDate: params.endDate }),
|
|
24
36
|
...(params.subject !== undefined && { subject: params.subject }),
|
package/dist/tools/registry.js
CHANGED
|
@@ -6,6 +6,8 @@ import { registerGetMessage } from './email/getMessage.js';
|
|
|
6
6
|
import { registerSendMessage } from './email/sendMessage.js';
|
|
7
7
|
import { registerReplyMessage } from './email/replyMessage.js';
|
|
8
8
|
import { registerMoveMessage } from './email/moveMessage.js';
|
|
9
|
+
import { registerListMessageAttachments } from './email/listMessageAttachments.js';
|
|
10
|
+
import { registerDownloadAttachment } from './email/downloadAttachment.js';
|
|
9
11
|
// Calendar tools
|
|
10
12
|
import { registerListCalendars } from './calendar/listCalendars.js';
|
|
11
13
|
import { registerListEvents } from './calendar/listEvents.js';
|
|
@@ -20,6 +22,8 @@ function registerEmailTools(server, config, auth) {
|
|
|
20
22
|
registerSendMessage(server, config, auth);
|
|
21
23
|
registerReplyMessage(server, config, auth);
|
|
22
24
|
registerMoveMessage(server, config, auth);
|
|
25
|
+
registerListMessageAttachments(server, config, auth);
|
|
26
|
+
registerDownloadAttachment(server, config, auth);
|
|
23
27
|
}
|
|
24
28
|
function registerCalendarTools(server, config, auth) {
|
|
25
29
|
registerListCalendars(server, config, auth);
|
package/dist/tools/schemas.js
CHANGED
|
@@ -19,10 +19,19 @@ export const ListMessagesSchema = AccountMailbox.extend({
|
|
|
19
19
|
bodySearch: z.string().optional().describe('Full-text search in message body. Note: cannot be combined with $filter — date/sender filters are applied client-side when this is used.'),
|
|
20
20
|
sender: z.string().email().optional().describe('Filter messages from this sender email address'),
|
|
21
21
|
maxResults: z.number().int().min(1).max(100).default(25).describe('Maximum number of messages to return (1–100)'),
|
|
22
|
+
select: z.array(z.string()).optional().describe('Fields to return. Omit for defaults (subject, from, receivedDateTime, isRead, hasAttachments, bodyPreview). Valid fields: id, subject, from, toRecipients, ccRecipients, bccRecipients, receivedDateTime, sentDateTime, bodyPreview, body, hasAttachments, attachmentCount, importance, isRead, isDraft, webLink, parentFolderId, conversationId'),
|
|
22
23
|
});
|
|
23
24
|
export const GetMessageSchema = AccountMailbox.extend({
|
|
24
25
|
messageId: z.string().describe('The message ID'),
|
|
25
26
|
includeBody: z.boolean().default(true).describe('Whether to include the full message body in the response'),
|
|
27
|
+
includeAttachments: z.boolean().default(false).describe('Whether to expand and include attachment metadata in the response'),
|
|
28
|
+
});
|
|
29
|
+
export const ListMessageAttachmentsSchema = AccountMailbox.extend({
|
|
30
|
+
messageId: z.string().describe('The message ID to list attachments for'),
|
|
31
|
+
});
|
|
32
|
+
export const DownloadAttachmentSchema = AccountMailbox.extend({
|
|
33
|
+
messageId: z.string().describe('The message ID'),
|
|
34
|
+
attachmentId: z.string().describe('The attachment ID to download'),
|
|
26
35
|
});
|
|
27
36
|
export const SendMessageSchema = AccountMailbox.extend({
|
|
28
37
|
to: z.array(z.string().email()).min(1).describe('List of recipient email addresses'),
|
|
@@ -50,6 +59,7 @@ export const ListEventsSchema = AccountMailbox.extend({
|
|
|
50
59
|
startDate: z.string().datetime().describe('Start of the date range (ISO 8601). Recurring events are expanded within this window.'),
|
|
51
60
|
endDate: z.string().datetime().describe('End of the date range (ISO 8601)'),
|
|
52
61
|
maxResults: z.number().int().min(1).max(100).default(25),
|
|
62
|
+
select: z.array(z.string()).optional().describe('Fields to return. Omit for defaults (subject, start, end, location, isOnlineMeeting). Valid fields: id, subject, start, end, location, isOnlineMeeting, onlineMeetingUrl, organizer, attendees, body, bodyPreview, categories, isRecurring, recurrence, sensitivity, showAs, uid'),
|
|
53
63
|
});
|
|
54
64
|
export const GetEventSchema = AccountMailbox.extend({
|
|
55
65
|
eventId: z.string().describe('The event ID'),
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// Allowed $select field lists and validation for list_messages and list_events.
|
|
2
|
+
// id is always forced into the select regardless of what the caller requests.
|
|
3
|
+
export const MESSAGE_SELECT_FIELDS = new Set([
|
|
4
|
+
'id',
|
|
5
|
+
'subject',
|
|
6
|
+
'from',
|
|
7
|
+
'toRecipients',
|
|
8
|
+
'ccRecipients',
|
|
9
|
+
'bccRecipients',
|
|
10
|
+
'receivedDateTime',
|
|
11
|
+
'sentDateTime',
|
|
12
|
+
'bodyPreview',
|
|
13
|
+
'body',
|
|
14
|
+
'hasAttachments',
|
|
15
|
+
'attachmentCount',
|
|
16
|
+
'importance',
|
|
17
|
+
'isRead',
|
|
18
|
+
'isDraft',
|
|
19
|
+
'webLink',
|
|
20
|
+
'parentFolderId',
|
|
21
|
+
'conversationId',
|
|
22
|
+
]);
|
|
23
|
+
export const EVENT_SELECT_FIELDS = new Set([
|
|
24
|
+
'id',
|
|
25
|
+
'subject',
|
|
26
|
+
'start',
|
|
27
|
+
'end',
|
|
28
|
+
'location',
|
|
29
|
+
'isOnlineMeeting',
|
|
30
|
+
'onlineMeetingUrl',
|
|
31
|
+
'organizer',
|
|
32
|
+
'attendees',
|
|
33
|
+
'body',
|
|
34
|
+
'bodyPreview',
|
|
35
|
+
'categories',
|
|
36
|
+
'isRecurring',
|
|
37
|
+
'recurrence',
|
|
38
|
+
'sensitivity',
|
|
39
|
+
'showAs',
|
|
40
|
+
'uid',
|
|
41
|
+
]);
|
|
42
|
+
export const MESSAGE_SELECT_DEFAULT = [
|
|
43
|
+
'subject',
|
|
44
|
+
'from',
|
|
45
|
+
'receivedDateTime',
|
|
46
|
+
'isRead',
|
|
47
|
+
'hasAttachments',
|
|
48
|
+
'bodyPreview',
|
|
49
|
+
];
|
|
50
|
+
export const EVENT_SELECT_DEFAULT = [
|
|
51
|
+
'subject',
|
|
52
|
+
'start',
|
|
53
|
+
'end',
|
|
54
|
+
'location',
|
|
55
|
+
'isOnlineMeeting',
|
|
56
|
+
];
|
|
57
|
+
/**
|
|
58
|
+
* Validate, normalise, and force-include `id` in a select list.
|
|
59
|
+
* Returns the final comma-separated string for Graph API $select,
|
|
60
|
+
* or throws with a descriptive message listing valid fields.
|
|
61
|
+
*/
|
|
62
|
+
export function resolveSelect(rawSelect, allowed, defaults) {
|
|
63
|
+
// Empty array → treat as omitted, use defaults
|
|
64
|
+
const fields = rawSelect && rawSelect.length > 0
|
|
65
|
+
? rawSelect.map((f) => f.trim()).filter(Boolean)
|
|
66
|
+
: defaults;
|
|
67
|
+
// Validate each field
|
|
68
|
+
for (const field of fields) {
|
|
69
|
+
if (!allowed.has(field)) {
|
|
70
|
+
throw new SelectValidationError(field, allowed);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
// id must always be present
|
|
74
|
+
const withId = fields.includes('id') ? fields : ['id', ...fields];
|
|
75
|
+
return withId.join(',');
|
|
76
|
+
}
|
|
77
|
+
export class SelectValidationError extends Error {
|
|
78
|
+
constructor(invalidField, allowed) {
|
|
79
|
+
const validList = [...allowed].sort().join(', ');
|
|
80
|
+
super(`Invalid select field: "${invalidField}". Valid fields: ${validList}`);
|
|
81
|
+
this.name = 'SelectValidationError';
|
|
82
|
+
}
|
|
83
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@teamnetwork/m365-mcp-server",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"description": "MCP server for Microsoft 365 Email and Calendar access using client credentials (app-only auth). Supports multiple accounts, shared mailboxes, per-mailbox read-only access, folder navigation, and date/content filtering.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|