outlook-file-attach-mcp 1.0.2
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 +66 -0
- package/index.js +206 -0
- package/package.json +29 -0
package/README.md
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# Outlook File Attach MCP Server
|
|
2
|
+
|
|
3
|
+
A lightweight MCP server that attaches local files to Outlook draft emails via the Microsoft Graph API.
|
|
4
|
+
|
|
5
|
+
## Why this exists
|
|
6
|
+
|
|
7
|
+
MCP-based email tools like `@softeria/ms-365-mcp-server` require file content to be passed as base64 through tool parameters. For files larger than ~50KB, this exceeds what an LLM can output in a single tool call. This server solves the problem by accepting a **local file path** instead — it reads the file from disk, encodes it, and uploads it directly to the Graph API.
|
|
8
|
+
|
|
9
|
+
## Requirements
|
|
10
|
+
|
|
11
|
+
- **Node.js 18+**
|
|
12
|
+
- **An Azure App Registration** with Application-level `Mail.ReadWrite` permission (with admin consent). This server uses client credentials flow, not delegated auth.
|
|
13
|
+
- **Local filesystem access** — the MCP host (e.g. Claude Desktop, Claude Cowork) needs read access to the folder containing the files you want to attach. In Claude Cowork, this means selecting a workspace folder. In Claude Desktop, the file paths must be accessible from the machine running the MCP server.
|
|
14
|
+
|
|
15
|
+
## Setup
|
|
16
|
+
|
|
17
|
+
Add the following to your MCP config (e.g. `claude_desktop_config.json`) under `mcpServers`:
|
|
18
|
+
|
|
19
|
+
```json
|
|
20
|
+
"outlook-file-attach": {
|
|
21
|
+
"command": "npx",
|
|
22
|
+
"args": ["-y", "outlook-file-attach-mcp"],
|
|
23
|
+
"env": {
|
|
24
|
+
"MS365_MCP_CLIENT_ID": "your-client-id",
|
|
25
|
+
"MS365_MCP_CLIENT_SECRET": "your-client-secret",
|
|
26
|
+
"MS365_MCP_TENANT_ID": "your-tenant-id"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Then restart your MCP host to load the server.
|
|
32
|
+
|
|
33
|
+
## Usage
|
|
34
|
+
|
|
35
|
+
The server provides one tool: **`attach-file-to-email`**
|
|
36
|
+
|
|
37
|
+
Parameters:
|
|
38
|
+
|
|
39
|
+
- `userEmail` — The mailbox owner's email address (e.g. `user@example.com`)
|
|
40
|
+
- `messageId` — The Outlook message ID of the draft to attach the file to
|
|
41
|
+
- `filePath` — Absolute path to the local file (e.g. `C:/Users/me/Documents/report.pdf`)
|
|
42
|
+
- `fileName` (optional) — Custom filename for the attachment. Defaults to the original filename.
|
|
43
|
+
|
|
44
|
+
Supported file types include PDF, DOCX, XLSX, PPTX, PNG, JPG, CSV, TXT, ZIP, and more. Unrecognized extensions are sent as `application/octet-stream`.
|
|
45
|
+
|
|
46
|
+
## How it works
|
|
47
|
+
|
|
48
|
+
1. The LLM calls `attach-file-to-email` with a local file path and a draft message ID
|
|
49
|
+
2. The server reads the file from disk and base64-encodes it
|
|
50
|
+
3. It authenticates using client credentials flow (no user interaction needed)
|
|
51
|
+
4. It POSTs the encoded file to the Microsoft Graph API as an attachment
|
|
52
|
+
5. The file appears on the draft email, ready to send
|
|
53
|
+
|
|
54
|
+
## Typical workflow
|
|
55
|
+
|
|
56
|
+
This server is designed to work alongside an MS365 MCP server that handles email composition. A typical flow looks like:
|
|
57
|
+
|
|
58
|
+
1. Create a draft email (using your MS365 MCP server)
|
|
59
|
+
2. Generate or locate a file locally (e.g. a report, spreadsheet, or PDF)
|
|
60
|
+
3. Attach the file to the draft using this server
|
|
61
|
+
4. Send the draft (using your MS365 MCP server)
|
|
62
|
+
|
|
63
|
+
## Limitations
|
|
64
|
+
|
|
65
|
+
- Maximum attachment size is 4MB per file (Microsoft Graph API limit for single-request uploads). For larger files, the resumable upload API would be needed.
|
|
66
|
+
- Requires Application-level permissions, not delegated. This means the Azure app can access any mailbox in the tenant — scope access appropriately.
|
package/index.js
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Outlook File Attach MCP Server
|
|
5
|
+
*
|
|
6
|
+
* A minimal MCP server that attaches local files to Outlook draft emails
|
|
7
|
+
* via the Microsoft Graph API. This solves the problem where Claude cannot
|
|
8
|
+
* pass large base64-encoded file content through tool parameters.
|
|
9
|
+
*
|
|
10
|
+
* The server reads the file from the local filesystem, base64-encodes it,
|
|
11
|
+
* and POSTs it to the Graph API using client credentials flow.
|
|
12
|
+
*
|
|
13
|
+
* Required environment variables:
|
|
14
|
+
* MS365_MCP_CLIENT_ID - Azure App Registration client ID
|
|
15
|
+
* MS365_MCP_CLIENT_SECRET - Azure App Registration client secret
|
|
16
|
+
* MS365_MCP_TENANT_ID - Azure tenant ID
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
20
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
21
|
+
import { z } from "zod";
|
|
22
|
+
import { readFileSync } from "fs";
|
|
23
|
+
import { basename } from "path";
|
|
24
|
+
import https from "https";
|
|
25
|
+
|
|
26
|
+
// --- Configuration from environment variables ---
|
|
27
|
+
const CLIENT_ID = process.env.MS365_MCP_CLIENT_ID;
|
|
28
|
+
const CLIENT_SECRET = process.env.MS365_MCP_CLIENT_SECRET;
|
|
29
|
+
const TENANT_ID = process.env.MS365_MCP_TENANT_ID;
|
|
30
|
+
|
|
31
|
+
// --- Helper: HTTPS request as a promise ---
|
|
32
|
+
function httpsRequest(url, options, body = null) {
|
|
33
|
+
return new Promise((resolve, reject) => {
|
|
34
|
+
const req = https.request(url, options, (res) => {
|
|
35
|
+
let data = "";
|
|
36
|
+
res.on("data", (chunk) => (data += chunk));
|
|
37
|
+
res.on("end", () => {
|
|
38
|
+
resolve({ statusCode: res.statusCode, headers: res.headers, body: data });
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
req.on("error", reject);
|
|
42
|
+
if (body) req.write(body);
|
|
43
|
+
req.end();
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// --- Get access token using client credentials flow ---
|
|
48
|
+
async function getAccessToken() {
|
|
49
|
+
const tokenUrl = `https://login.microsoftonline.com/${TENANT_ID}/oauth2/v2.0/token`;
|
|
50
|
+
const params = new URLSearchParams({
|
|
51
|
+
client_id: CLIENT_ID,
|
|
52
|
+
client_secret: CLIENT_SECRET,
|
|
53
|
+
scope: "https://graph.microsoft.com/.default",
|
|
54
|
+
grant_type: "client_credentials",
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const urlObj = new URL(tokenUrl);
|
|
58
|
+
const body = params.toString();
|
|
59
|
+
|
|
60
|
+
const response = await httpsRequest(tokenUrl, {
|
|
61
|
+
hostname: urlObj.hostname,
|
|
62
|
+
path: urlObj.pathname,
|
|
63
|
+
method: "POST",
|
|
64
|
+
headers: {
|
|
65
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
66
|
+
"Content-Length": Buffer.byteLength(body),
|
|
67
|
+
},
|
|
68
|
+
}, body);
|
|
69
|
+
|
|
70
|
+
if (response.statusCode !== 200) {
|
|
71
|
+
throw new Error(`Token request failed (${response.statusCode}): ${response.body}`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const tokenData = JSON.parse(response.body);
|
|
75
|
+
if (!tokenData.access_token) {
|
|
76
|
+
throw new Error(`No access token in response: ${response.body}`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return tokenData.access_token;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// --- Attach file to a message via Graph API ---
|
|
83
|
+
async function attachFileToMessage(token, userEmail, messageId, filePath, fileName) {
|
|
84
|
+
// Read the file and base64-encode it
|
|
85
|
+
const fileBuffer = readFileSync(filePath);
|
|
86
|
+
const base64Content = fileBuffer.toString("base64");
|
|
87
|
+
|
|
88
|
+
// Determine file name from path if not provided
|
|
89
|
+
const attachmentName = fileName || basename(filePath);
|
|
90
|
+
|
|
91
|
+
// Determine content type based on extension
|
|
92
|
+
const ext = attachmentName.split(".").pop().toLowerCase();
|
|
93
|
+
const contentTypes = {
|
|
94
|
+
pdf: "application/pdf",
|
|
95
|
+
doc: "application/msword",
|
|
96
|
+
docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
97
|
+
xls: "application/vnd.ms-excel",
|
|
98
|
+
xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
99
|
+
ppt: "application/vnd.ms-powerpoint",
|
|
100
|
+
pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
101
|
+
png: "image/png",
|
|
102
|
+
jpg: "image/jpeg",
|
|
103
|
+
jpeg: "image/jpeg",
|
|
104
|
+
gif: "image/gif",
|
|
105
|
+
txt: "text/plain",
|
|
106
|
+
csv: "text/csv",
|
|
107
|
+
zip: "application/zip",
|
|
108
|
+
};
|
|
109
|
+
const contentType = contentTypes[ext] || "application/octet-stream";
|
|
110
|
+
|
|
111
|
+
// Build the attachment payload
|
|
112
|
+
const payload = JSON.stringify({
|
|
113
|
+
"@odata.type": "#microsoft.graph.fileAttachment",
|
|
114
|
+
name: attachmentName,
|
|
115
|
+
contentType: contentType,
|
|
116
|
+
contentBytes: base64Content,
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// POST to Graph API
|
|
120
|
+
const graphUrl = `https://graph.microsoft.com/v1.0/users/${encodeURIComponent(userEmail)}/messages/${encodeURIComponent(messageId)}/attachments`;
|
|
121
|
+
const urlObj = new URL(graphUrl);
|
|
122
|
+
|
|
123
|
+
const response = await httpsRequest(graphUrl, {
|
|
124
|
+
hostname: urlObj.hostname,
|
|
125
|
+
path: urlObj.pathname,
|
|
126
|
+
method: "POST",
|
|
127
|
+
headers: {
|
|
128
|
+
Authorization: `Bearer ${token}`,
|
|
129
|
+
"Content-Type": "application/json",
|
|
130
|
+
"Content-Length": Buffer.byteLength(payload),
|
|
131
|
+
},
|
|
132
|
+
}, payload);
|
|
133
|
+
|
|
134
|
+
if (response.statusCode !== 200 && response.statusCode !== 201) {
|
|
135
|
+
throw new Error(`Attachment upload failed (${response.statusCode}): ${response.body}`);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return JSON.parse(response.body);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// --- Main: Create and start MCP server ---
|
|
142
|
+
const server = new McpServer({
|
|
143
|
+
name: "outlook-file-attach",
|
|
144
|
+
version: "1.0.0",
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
server.tool(
|
|
148
|
+
"attach-file-to-email",
|
|
149
|
+
"Attach a local file to an existing Outlook draft email. Reads the file from the local filesystem, " +
|
|
150
|
+
"base64-encodes it, and uploads it to the specified draft message via Microsoft Graph API. " +
|
|
151
|
+
"This tool solves the problem of attaching files that are too large to pass as base64 through tool parameters.",
|
|
152
|
+
{
|
|
153
|
+
userEmail: z.string().describe("Email address of the mailbox owner (e.g. 'user@company.com')"),
|
|
154
|
+
messageId: z.string().describe("The Outlook message ID of the draft email to attach the file to"),
|
|
155
|
+
filePath: z.string().describe("Absolute path to the local file to attach (e.g. 'C:/Users/user/Documents/file.pdf')"),
|
|
156
|
+
fileName: z.string().optional().describe("Optional custom filename for the attachment. If omitted, uses the original filename from the path."),
|
|
157
|
+
},
|
|
158
|
+
async ({ userEmail, messageId, filePath, fileName }) => {
|
|
159
|
+
// Validate environment
|
|
160
|
+
if (!CLIENT_ID || !CLIENT_SECRET || !TENANT_ID) {
|
|
161
|
+
return {
|
|
162
|
+
content: [{
|
|
163
|
+
type: "text",
|
|
164
|
+
text: "Error: Missing required environment variables. Ensure MS365_MCP_CLIENT_ID, MS365_MCP_CLIENT_SECRET, and MS365_MCP_TENANT_ID are set.",
|
|
165
|
+
}],
|
|
166
|
+
isError: true,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
try {
|
|
171
|
+
// Step 1: Get access token
|
|
172
|
+
const token = await getAccessToken();
|
|
173
|
+
|
|
174
|
+
// Step 2: Attach the file
|
|
175
|
+
const result = await attachFileToMessage(token, userEmail, messageId, filePath, fileName);
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
content: [{
|
|
179
|
+
type: "text",
|
|
180
|
+
text: JSON.stringify({
|
|
181
|
+
success: true,
|
|
182
|
+
message: `File "${result.name}" attached successfully.`,
|
|
183
|
+
attachment: {
|
|
184
|
+
id: result.id,
|
|
185
|
+
name: result.name,
|
|
186
|
+
size: result.size,
|
|
187
|
+
contentType: result.contentType,
|
|
188
|
+
},
|
|
189
|
+
}, null, 2),
|
|
190
|
+
}],
|
|
191
|
+
};
|
|
192
|
+
} catch (error) {
|
|
193
|
+
return {
|
|
194
|
+
content: [{
|
|
195
|
+
type: "text",
|
|
196
|
+
text: `Error attaching file: ${error.message}`,
|
|
197
|
+
}],
|
|
198
|
+
isError: true,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
// Start the server
|
|
205
|
+
const transport = new StdioServerTransport();
|
|
206
|
+
await server.connect(transport);
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "outlook-file-attach-mcp",
|
|
3
|
+
"version": "1.0.2",
|
|
4
|
+
"description": "MCP server that attaches local files to Outlook draft emails via Microsoft Graph API",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"outlook-file-attach-mcp": "index.js"
|
|
9
|
+
},
|
|
10
|
+
"keywords": [
|
|
11
|
+
"mcp",
|
|
12
|
+
"outlook",
|
|
13
|
+
"email",
|
|
14
|
+
"attachment",
|
|
15
|
+
"microsoft-graph"
|
|
16
|
+
],
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"files": [
|
|
19
|
+
"index.js",
|
|
20
|
+
"README.md"
|
|
21
|
+
],
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": ">=18"
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
27
|
+
"zod": "^3.25.0"
|
|
28
|
+
}
|
|
29
|
+
}
|