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.
Files changed (3) hide show
  1. package/README.md +66 -0
  2. package/index.js +206 -0
  3. 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
+ }