@teamnetwork/m365-mcp-server 1.0.2 → 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/email.js +14 -7
- 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/registry.js +4 -0
- package/dist/tools/schemas.js +8 -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/email.js
CHANGED
|
@@ -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');
|
|
@@ -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
|
+
}
|
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
|
@@ -24,6 +24,14 @@ export const ListMessagesSchema = AccountMailbox.extend({
|
|
|
24
24
|
export const GetMessageSchema = AccountMailbox.extend({
|
|
25
25
|
messageId: z.string().describe('The message ID'),
|
|
26
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'),
|
|
27
35
|
});
|
|
28
36
|
export const SendMessageSchema = AccountMailbox.extend({
|
|
29
37
|
to: z.array(z.string().email()).min(1).describe('List of recipient email addresses'),
|
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",
|