@tgai96/outlook-mcp 1.0.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.
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Folder management module for Outlook MCP server
3
+ */
4
+ const handleListFolders = require('./list');
5
+ const handleCreateFolder = require('./create');
6
+ const handleMoveEmails = require('./move');
7
+
8
+ // Folder management tool definitions
9
+ const folderTools = [
10
+ {
11
+ name: "list-folders",
12
+ description: "Lists mail folders in your Outlook account",
13
+ inputSchema: {
14
+ type: "object",
15
+ properties: {
16
+ includeItemCounts: {
17
+ type: "boolean",
18
+ description: "Include counts of total and unread items"
19
+ },
20
+ includeChildren: {
21
+ type: "boolean",
22
+ description: "Include child folders in hierarchy"
23
+ }
24
+ },
25
+ required: []
26
+ },
27
+ handler: handleListFolders
28
+ },
29
+ {
30
+ name: "create-folder",
31
+ description: "Creates a new mail folder",
32
+ inputSchema: {
33
+ type: "object",
34
+ properties: {
35
+ name: {
36
+ type: "string",
37
+ description: "Name of the folder to create"
38
+ },
39
+ parentFolder: {
40
+ type: "string",
41
+ description: "Optional parent folder name (default is root)"
42
+ }
43
+ },
44
+ required: ["name"]
45
+ },
46
+ handler: handleCreateFolder
47
+ },
48
+ {
49
+ name: "move-emails",
50
+ description: "Moves emails from one folder to another",
51
+ inputSchema: {
52
+ type: "object",
53
+ properties: {
54
+ emailIds: {
55
+ type: "string",
56
+ description: "Comma-separated list of email IDs to move"
57
+ },
58
+ targetFolder: {
59
+ type: "string",
60
+ description: "Name of the folder to move emails to"
61
+ },
62
+ sourceFolder: {
63
+ type: "string",
64
+ description: "Optional name of the source folder (default is inbox)"
65
+ }
66
+ },
67
+ required: ["emailIds", "targetFolder"]
68
+ },
69
+ handler: handleMoveEmails
70
+ }
71
+ ];
72
+
73
+ module.exports = {
74
+ folderTools,
75
+ handleListFolders,
76
+ handleCreateFolder,
77
+ handleMoveEmails
78
+ };
package/folder/list.js ADDED
@@ -0,0 +1,264 @@
1
+ /**
2
+ * List folders functionality
3
+ */
4
+ const { callGraphAPI } = require('../utils/graph-api');
5
+ const { ensureAuthenticated } = require('../auth');
6
+
7
+ /**
8
+ * List folders handler
9
+ * @param {object} args - Tool arguments
10
+ * @returns {object} - MCP response
11
+ */
12
+ async function handleListFolders(args) {
13
+ const includeItemCounts = args.includeItemCounts === true;
14
+ const includeChildren = args.includeChildren === true;
15
+
16
+ try {
17
+ // Get access token
18
+ const accessToken = await ensureAuthenticated();
19
+
20
+ // Get all mail folders
21
+ const folders = await getAllFoldersHierarchy(accessToken, includeItemCounts);
22
+
23
+ // If including children, format as hierarchy
24
+ if (includeChildren) {
25
+ return {
26
+ content: [{
27
+ type: "text",
28
+ text: formatFolderHierarchy(folders, includeItemCounts)
29
+ }]
30
+ };
31
+ } else {
32
+ // Otherwise, format as flat list
33
+ return {
34
+ content: [{
35
+ type: "text",
36
+ text: formatFolderList(folders, includeItemCounts)
37
+ }]
38
+ };
39
+ }
40
+ } catch (error) {
41
+ if (error.message === 'Authentication required') {
42
+ return {
43
+ content: [{
44
+ type: "text",
45
+ text: "Authentication required. Please use the 'authenticate' tool first."
46
+ }]
47
+ };
48
+ }
49
+
50
+ return {
51
+ content: [{
52
+ type: "text",
53
+ text: `Error listing folders: ${error.message}`
54
+ }]
55
+ };
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Get all mail folders with hierarchy information
61
+ * @param {string} accessToken - Access token
62
+ * @param {boolean} includeItemCounts - Include item counts in response
63
+ * @returns {Promise<Array>} - Array of folder objects with hierarchy
64
+ */
65
+ async function getAllFoldersHierarchy(accessToken, includeItemCounts) {
66
+ try {
67
+ // Determine select fields based on whether to include counts
68
+ const selectFields = includeItemCounts
69
+ ? 'id,displayName,parentFolderId,childFolderCount,totalItemCount,unreadItemCount'
70
+ : 'id,displayName,parentFolderId,childFolderCount';
71
+
72
+ // Get all mail folders
73
+ const response = await callGraphAPI(
74
+ accessToken,
75
+ 'GET',
76
+ 'me/mailFolders',
77
+ null,
78
+ {
79
+ $top: 100,
80
+ $select: selectFields
81
+ }
82
+ );
83
+
84
+ if (!response.value) {
85
+ return [];
86
+ }
87
+
88
+ // Get child folders for folders with children
89
+ const foldersWithChildren = response.value.filter(f => f.childFolderCount > 0);
90
+
91
+ const childFolderPromises = foldersWithChildren.map(async (folder) => {
92
+ try {
93
+ const childResponse = await callGraphAPI(
94
+ accessToken,
95
+ 'GET',
96
+ `me/mailFolders/${folder.id}/childFolders`,
97
+ null,
98
+ { $select: selectFields }
99
+ );
100
+
101
+ // Add parent folder info to each child
102
+ const childFolders = childResponse.value || [];
103
+ childFolders.forEach(child => {
104
+ child.parentFolder = folder.displayName;
105
+ });
106
+
107
+ return childFolders;
108
+ } catch (error) {
109
+ console.error(`Error getting child folders for "${folder.displayName}": ${error.message}`);
110
+ return [];
111
+ }
112
+ });
113
+
114
+ const childFolders = await Promise.all(childFolderPromises);
115
+ const allChildFolders = childFolders.flat();
116
+
117
+ // Add top-level flag to parent folders
118
+ const topLevelFolders = response.value.map(folder => ({
119
+ ...folder,
120
+ isTopLevel: true
121
+ }));
122
+
123
+ // Combine all folders
124
+ return [...topLevelFolders, ...allChildFolders];
125
+ } catch (error) {
126
+ console.error(`Error getting all folders: ${error.message}`);
127
+ throw error;
128
+ }
129
+ }
130
+
131
+ /**
132
+ * Format folders as a flat list
133
+ * @param {Array} folders - Array of folder objects
134
+ * @param {boolean} includeItemCounts - Whether to include item counts
135
+ * @returns {string} - Formatted list
136
+ */
137
+ function formatFolderList(folders, includeItemCounts) {
138
+ if (!folders || folders.length === 0) {
139
+ return "No folders found.";
140
+ }
141
+
142
+ // Sort folders alphabetically, with well-known folders first
143
+ const wellKnownFolderNames = ['Inbox', 'Drafts', 'Sent Items', 'Deleted Items', 'Junk Email', 'Archive'];
144
+
145
+ const sortedFolders = [...folders].sort((a, b) => {
146
+ // Well-known folders come first
147
+ const aIsWellKnown = wellKnownFolderNames.includes(a.displayName);
148
+ const bIsWellKnown = wellKnownFolderNames.includes(b.displayName);
149
+
150
+ if (aIsWellKnown && !bIsWellKnown) return -1;
151
+ if (!aIsWellKnown && bIsWellKnown) return 1;
152
+
153
+ if (aIsWellKnown && bIsWellKnown) {
154
+ // Sort well-known folders by their index in the array
155
+ return wellKnownFolderNames.indexOf(a.displayName) - wellKnownFolderNames.indexOf(b.displayName);
156
+ }
157
+
158
+ // Sort other folders alphabetically
159
+ return a.displayName.localeCompare(b.displayName);
160
+ });
161
+
162
+ // Format each folder
163
+ const folderLines = sortedFolders.map(folder => {
164
+ let folderInfo = folder.displayName;
165
+
166
+ // Add parent folder info if available
167
+ if (folder.parentFolder) {
168
+ folderInfo += ` (in ${folder.parentFolder})`;
169
+ }
170
+
171
+ // Add item counts if requested
172
+ if (includeItemCounts) {
173
+ const unreadCount = folder.unreadItemCount || 0;
174
+ const totalCount = folder.totalItemCount || 0;
175
+ folderInfo += ` - ${totalCount} items`;
176
+
177
+ if (unreadCount > 0) {
178
+ folderInfo += ` (${unreadCount} unread)`;
179
+ }
180
+ }
181
+
182
+ return folderInfo;
183
+ });
184
+
185
+ return `Found ${folders.length} folders:\n\n${folderLines.join('\n')}`;
186
+ }
187
+
188
+ /**
189
+ * Format folders as a hierarchical tree
190
+ * @param {Array} folders - Array of folder objects
191
+ * @param {boolean} includeItemCounts - Whether to include item counts
192
+ * @returns {string} - Formatted hierarchy
193
+ */
194
+ function formatFolderHierarchy(folders, includeItemCounts) {
195
+ if (!folders || folders.length === 0) {
196
+ return "No folders found.";
197
+ }
198
+
199
+ // Build folder hierarchy
200
+ const folderMap = new Map();
201
+ const rootFolders = [];
202
+
203
+ // First pass: create map of all folders
204
+ folders.forEach(folder => {
205
+ folderMap.set(folder.id, {
206
+ ...folder,
207
+ children: []
208
+ });
209
+
210
+ if (folder.isTopLevel) {
211
+ rootFolders.push(folder.id);
212
+ }
213
+ });
214
+
215
+ // Second pass: build hierarchy
216
+ folders.forEach(folder => {
217
+ if (!folder.isTopLevel && folder.parentFolderId) {
218
+ const parent = folderMap.get(folder.parentFolderId);
219
+ if (parent) {
220
+ parent.children.push(folder.id);
221
+ } else {
222
+ // Fallback for orphaned folders
223
+ rootFolders.push(folder.id);
224
+ }
225
+ }
226
+ });
227
+
228
+ // Format hierarchy recursively
229
+ function formatSubtree(folderId, level = 0) {
230
+ const folder = folderMap.get(folderId);
231
+ if (!folder) return '';
232
+
233
+ const indent = ' '.repeat(level);
234
+ let line = `${indent}${folder.displayName}`;
235
+
236
+ // Add item counts if requested
237
+ if (includeItemCounts) {
238
+ const unreadCount = folder.unreadItemCount || 0;
239
+ const totalCount = folder.totalItemCount || 0;
240
+ line += ` - ${totalCount} items`;
241
+
242
+ if (unreadCount > 0) {
243
+ line += ` (${unreadCount} unread)`;
244
+ }
245
+ }
246
+
247
+ // Add children
248
+ const childLines = folder.children
249
+ .map(childId => formatSubtree(childId, level + 1))
250
+ .filter(line => line.length > 0)
251
+ .join('\n');
252
+
253
+ return childLines.length > 0 ? `${line}\n${childLines}` : line;
254
+ }
255
+
256
+ // Format all root folders
257
+ const formattedHierarchy = rootFolders
258
+ .map(folderId => formatSubtree(folderId))
259
+ .join('\n');
260
+
261
+ return `Folder Hierarchy:\n\n${formattedHierarchy}`;
262
+ }
263
+
264
+ module.exports = handleListFolders;
package/folder/move.js ADDED
@@ -0,0 +1,163 @@
1
+ /**
2
+ * Move emails functionality
3
+ */
4
+ const { callGraphAPI } = require('../utils/graph-api');
5
+ const { ensureAuthenticated } = require('../auth');
6
+ const { getFolderIdByName } = require('../email/folder-utils');
7
+
8
+ /**
9
+ * Move emails handler
10
+ * @param {object} args - Tool arguments
11
+ * @returns {object} - MCP response
12
+ */
13
+ async function handleMoveEmails(args) {
14
+ const emailIds = args.emailIds || '';
15
+ const targetFolder = args.targetFolder || '';
16
+ const sourceFolder = args.sourceFolder || '';
17
+
18
+ if (!emailIds) {
19
+ return {
20
+ content: [{
21
+ type: "text",
22
+ text: "Email IDs are required. Please provide a comma-separated list of email IDs to move."
23
+ }]
24
+ };
25
+ }
26
+
27
+ if (!targetFolder) {
28
+ return {
29
+ content: [{
30
+ type: "text",
31
+ text: "Target folder name is required."
32
+ }]
33
+ };
34
+ }
35
+
36
+ try {
37
+ // Get access token
38
+ const accessToken = await ensureAuthenticated();
39
+
40
+ // Parse email IDs
41
+ const ids = emailIds.split(',').map(id => id.trim()).filter(id => id);
42
+
43
+ if (ids.length === 0) {
44
+ return {
45
+ content: [{
46
+ type: "text",
47
+ text: "No valid email IDs provided."
48
+ }]
49
+ };
50
+ }
51
+
52
+ // Move emails
53
+ const result = await moveEmailsToFolder(accessToken, ids, targetFolder, sourceFolder);
54
+
55
+ return {
56
+ content: [{
57
+ type: "text",
58
+ text: result.message
59
+ }]
60
+ };
61
+ } catch (error) {
62
+ if (error.message === 'Authentication required') {
63
+ return {
64
+ content: [{
65
+ type: "text",
66
+ text: "Authentication required. Please use the 'authenticate' tool first."
67
+ }]
68
+ };
69
+ }
70
+
71
+ return {
72
+ content: [{
73
+ type: "text",
74
+ text: `Error moving emails: ${error.message}`
75
+ }]
76
+ };
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Move emails to a folder
82
+ * @param {string} accessToken - Access token
83
+ * @param {Array<string>} emailIds - Array of email IDs to move
84
+ * @param {string} targetFolderName - Name of the target folder
85
+ * @param {string} sourceFolderName - Name of the source folder (optional)
86
+ * @returns {Promise<object>} - Result object with status and message
87
+ */
88
+ async function moveEmailsToFolder(accessToken, emailIds, targetFolderName, sourceFolderName) {
89
+ try {
90
+ // Get the target folder ID
91
+ const targetFolderId = await getFolderIdByName(accessToken, targetFolderName);
92
+ if (!targetFolderId) {
93
+ return {
94
+ success: false,
95
+ message: `Target folder "${targetFolderName}" not found. Please specify a valid folder name.`
96
+ };
97
+ }
98
+
99
+ // Track successful and failed moves
100
+ const results = {
101
+ successful: [],
102
+ failed: []
103
+ };
104
+
105
+ // Process each email one by one to handle errors independently
106
+ for (const emailId of emailIds) {
107
+ try {
108
+ // Move the email
109
+ await callGraphAPI(
110
+ accessToken,
111
+ 'POST',
112
+ `me/messages/${emailId}/move`,
113
+ {
114
+ destinationId: targetFolderId
115
+ }
116
+ );
117
+
118
+ results.successful.push(emailId);
119
+ } catch (error) {
120
+ console.error(`Error moving email ${emailId}: ${error.message}`);
121
+ results.failed.push({
122
+ id: emailId,
123
+ error: error.message
124
+ });
125
+ }
126
+ }
127
+
128
+ // Generate result message
129
+ let message = '';
130
+
131
+ if (results.successful.length > 0) {
132
+ message += `Successfully moved ${results.successful.length} email(s) to "${targetFolderName}".`;
133
+ }
134
+
135
+ if (results.failed.length > 0) {
136
+ if (message) message += '\n\n';
137
+ message += `Failed to move ${results.failed.length} email(s). Errors:`;
138
+
139
+ // Show first few errors with details
140
+ const maxErrors = Math.min(results.failed.length, 3);
141
+ for (let i = 0; i < maxErrors; i++) {
142
+ const failure = results.failed[i];
143
+ message += `\n- Email ${i+1}: ${failure.error}`;
144
+ }
145
+
146
+ // If there are more errors, just mention the count
147
+ if (results.failed.length > maxErrors) {
148
+ message += `\n...and ${results.failed.length - maxErrors} more.`;
149
+ }
150
+ }
151
+
152
+ return {
153
+ success: results.successful.length > 0,
154
+ message,
155
+ results
156
+ };
157
+ } catch (error) {
158
+ console.error(`Error in moveEmailsToFolder: ${error.message}`);
159
+ throw error;
160
+ }
161
+ }
162
+
163
+ module.exports = handleMoveEmails;
package/index.js ADDED
@@ -0,0 +1,148 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Outlook MCP Server - Main entry point
4
+ *
5
+ * A Model Context Protocol server that provides access to
6
+ * Microsoft Outlook through the Microsoft Graph API.
7
+ */
8
+ const { Server } = require("@modelcontextprotocol/sdk/server/index.js");
9
+ const { StdioServerTransport } = require("@modelcontextprotocol/sdk/server/stdio.js");
10
+ const config = require('./config');
11
+
12
+ // Import module tools
13
+ const { authTools } = require('./auth');
14
+ const { calendarTools } = require('./calendar');
15
+ const { emailTools } = require('./email');
16
+ const { folderTools } = require('./folder');
17
+ const { rulesTools } = require('./rules');
18
+
19
+ // Log startup information
20
+ console.error(`STARTING ${config.SERVER_NAME.toUpperCase()} MCP SERVER`);
21
+ console.error(`Test mode is ${config.USE_TEST_MODE ? 'enabled' : 'disabled'}`);
22
+
23
+ // Combine all tools
24
+ const TOOLS = [
25
+ ...authTools,
26
+ ...calendarTools,
27
+ ...emailTools,
28
+ ...folderTools,
29
+ ...rulesTools
30
+ // Future modules: contactsTools, etc.
31
+ ];
32
+
33
+ // Create server with tools capabilities
34
+ const server = new Server(
35
+ { name: config.SERVER_NAME, version: config.SERVER_VERSION },
36
+ {
37
+ capabilities: {
38
+ tools: TOOLS.reduce((acc, tool) => {
39
+ acc[tool.name] = {};
40
+ return acc;
41
+ }, {})
42
+ }
43
+ }
44
+ );
45
+
46
+ // Handle all requests
47
+ server.fallbackRequestHandler = async (request) => {
48
+ try {
49
+ const { method, params, id } = request;
50
+ console.error(`REQUEST: ${method} [${id}]`);
51
+
52
+ // Initialize handler
53
+ if (method === "initialize") {
54
+ console.error(`INITIALIZE REQUEST: ID [${id}]`);
55
+ return {
56
+ protocolVersion: "2024-11-05",
57
+ capabilities: {
58
+ tools: TOOLS.reduce((acc, tool) => {
59
+ acc[tool.name] = {};
60
+ return acc;
61
+ }, {})
62
+ },
63
+ serverInfo: { name: config.SERVER_NAME, version: config.SERVER_VERSION }
64
+ };
65
+ }
66
+
67
+ // Tools list handler
68
+ if (method === "tools/list") {
69
+ console.error(`TOOLS LIST REQUEST: ID [${id}]`);
70
+ console.error(`TOOLS COUNT: ${TOOLS.length}`);
71
+ console.error(`TOOLS NAMES: ${TOOLS.map(t => t.name).join(', ')}`);
72
+
73
+ return {
74
+ tools: TOOLS.map(tool => ({
75
+ name: tool.name,
76
+ description: tool.description,
77
+ inputSchema: tool.inputSchema
78
+ }))
79
+ };
80
+ }
81
+
82
+ // Required empty responses for other capabilities
83
+ if (method === "resources/list") return { resources: [] };
84
+ if (method === "prompts/list") return { prompts: [] };
85
+
86
+ // Tool call handler
87
+ if (method === "tools/call") {
88
+ try {
89
+ const { name, arguments: args = {} } = params || {};
90
+
91
+ console.error(`TOOL CALL: ${name}`);
92
+
93
+ // Find the tool handler
94
+ const tool = TOOLS.find(t => t.name === name);
95
+
96
+ if (tool && tool.handler) {
97
+ return await tool.handler(args);
98
+ }
99
+
100
+ // Tool not found
101
+ return {
102
+ error: {
103
+ code: -32601,
104
+ message: `Tool not found: ${name}`
105
+ }
106
+ };
107
+ } catch (error) {
108
+ console.error(`Error in tools/call:`, error);
109
+ return {
110
+ error: {
111
+ code: -32603,
112
+ message: `Error processing tool call: ${error.message}`
113
+ }
114
+ };
115
+ }
116
+ }
117
+
118
+ // For any other method, return method not found
119
+ return {
120
+ error: {
121
+ code: -32601,
122
+ message: `Method not found: ${method}`
123
+ }
124
+ };
125
+ } catch (error) {
126
+ console.error(`Error in fallbackRequestHandler:`, error);
127
+ return {
128
+ error: {
129
+ code: -32603,
130
+ message: `Error processing request: ${error.message}`
131
+ }
132
+ };
133
+ }
134
+ };
135
+
136
+ // Make the script executable
137
+ process.on('SIGTERM', () => {
138
+ console.error('SIGTERM received but staying alive');
139
+ });
140
+
141
+ // Start the server
142
+ const transport = new StdioServerTransport();
143
+ server.connect(transport)
144
+ .then(() => console.error(`${config.SERVER_NAME} connected and listening`))
145
+ .catch(error => {
146
+ console.error(`Connection error: ${error.message}`);
147
+ process.exit(1);
148
+ });
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@tgai96/outlook-mcp",
3
+ "version": "1.0.0",
4
+ "description": "MCP server for Claude to access Outlook data via Microsoft Graph API",
5
+ "main": "index.js",
6
+ "bin": {
7
+ "outlook-mcp": "./cli.js"
8
+ },
9
+ "files": [
10
+ "index.js",
11
+ "cli.js",
12
+ "config.js",
13
+ "auth/",
14
+ "calendar/",
15
+ "email/",
16
+ "folder/",
17
+ "rules/",
18
+ "utils/",
19
+ ".env.example",
20
+ "claude-config-sample.json"
21
+ ],
22
+ "scripts": {
23
+ "start": "node index.js",
24
+ "inspect": "npx @modelcontextprotocol/inspector node index.js",
25
+ "test-all-tools": "node test-mcp-tools.js",
26
+ "test": "jest"
27
+ },
28
+ "keywords": [
29
+ "claude",
30
+ "outlook",
31
+ "mcp",
32
+ "microsoft-graph",
33
+ "email",
34
+ "calendar",
35
+ "model-context-protocol"
36
+ ],
37
+ "author": "tgai96",
38
+ "license": "MIT",
39
+ "publishConfig": {
40
+ "access": "public"
41
+ },
42
+ "dependencies": {
43
+ "@modelcontextprotocol/sdk": "^1.1.0",
44
+ "dotenv": "^16.5.0"
45
+ },
46
+ "devDependencies": {
47
+ "@modelcontextprotocol/inspector": "^0.10.2",
48
+ "jest": "^29.7.0",
49
+ "supertest": "^7.0.0"
50
+ },
51
+ "engines": {
52
+ "node": ">=14.0.0"
53
+ }
54
+ }