@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.
package/config.js ADDED
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Configuration for Outlook MCP Server
3
+ */
4
+ const path = require('path');
5
+ const os = require('os');
6
+ const fs = require('fs');
7
+
8
+ // Ensure we have a home directory path even if process.env.HOME is undefined
9
+ const homeDir = process.env.HOME || process.env.USERPROFILE || os.homedir() || '/tmp';
10
+ const configDir = path.join(homeDir, '.outlook-mcp');
11
+ const configFilePath = path.join(configDir, 'config.json');
12
+
13
+ /**
14
+ * Load configuration from file
15
+ * Priority: Environment variables > Config file > Defaults
16
+ */
17
+ function loadConfig() {
18
+ let fileConfig = {};
19
+
20
+ // Try to load from config file
21
+ try {
22
+ if (fs.existsSync(configFilePath)) {
23
+ const configData = fs.readFileSync(configFilePath, 'utf8');
24
+ fileConfig = JSON.parse(configData);
25
+ }
26
+ } catch (error) {
27
+ console.error(`Warning: Could not load config from ${configFilePath}:`, error.message);
28
+ }
29
+
30
+ // Priority: env vars > file config > defaults
31
+ return {
32
+ // Test mode: env > file > default
33
+ USE_TEST_MODE: process.env.USE_TEST_MODE === 'true' || fileConfig.USE_TEST_MODE === true || fileConfig.USE_TEST_MODE === 'true',
34
+
35
+ // Client ID: env > file > default
36
+ OUTLOOK_CLIENT_ID: process.env.OUTLOOK_CLIENT_ID || fileConfig.MS_CLIENT_ID || fileConfig.OUTLOOK_CLIENT_ID || '',
37
+
38
+ // Client Secret: env > file > default
39
+ OUTLOOK_CLIENT_SECRET: process.env.OUTLOOK_CLIENT_SECRET || fileConfig.MS_CLIENT_SECRET || fileConfig.OUTLOOK_CLIENT_SECRET || '',
40
+
41
+ // MS_CLIENT_ID for auth server: env > file > default
42
+ MS_CLIENT_ID: process.env.MS_CLIENT_ID || fileConfig.MS_CLIENT_ID || '',
43
+
44
+ // MS_CLIENT_SECRET for auth server: env > file > default
45
+ MS_CLIENT_SECRET: process.env.MS_CLIENT_SECRET || fileConfig.MS_CLIENT_SECRET || '',
46
+ };
47
+ }
48
+
49
+ const loadedConfig = loadConfig();
50
+
51
+ module.exports = {
52
+ // Server information
53
+ SERVER_NAME: "outlook-assistant",
54
+ SERVER_VERSION: "1.0.0",
55
+
56
+ // Test mode setting
57
+ USE_TEST_MODE: loadedConfig.USE_TEST_MODE,
58
+
59
+ // Authentication configuration
60
+ AUTH_CONFIG: {
61
+ clientId: loadedConfig.OUTLOOK_CLIENT_ID || loadedConfig.MS_CLIENT_ID || '',
62
+ clientSecret: loadedConfig.OUTLOOK_CLIENT_SECRET || loadedConfig.MS_CLIENT_SECRET || '',
63
+ redirectUri: 'http://localhost:3333/auth/callback',
64
+ scopes: ['Mail.Read', 'Mail.ReadWrite', 'Mail.Send', 'User.Read', 'Calendars.Read', 'Calendars.ReadWrite'],
65
+ tokenStorePath: path.join(homeDir, '.outlook-mcp', 'tokens.json'),
66
+ authServerUrl: 'http://localhost:3333',
67
+ configFilePath: configFilePath
68
+ },
69
+
70
+ // Export config loading utilities
71
+ loadConfig: loadConfig,
72
+ saveConfig: function(config) {
73
+ try {
74
+ // Ensure directory exists
75
+ if (!fs.existsSync(configDir)) {
76
+ fs.mkdirSync(configDir, { recursive: true });
77
+ }
78
+
79
+ // Save config file
80
+ fs.writeFileSync(configFilePath, JSON.stringify(config, null, 2), 'utf8');
81
+ return true;
82
+ } catch (error) {
83
+ console.error(`Error saving config to ${configFilePath}:`, error.message);
84
+ return false;
85
+ }
86
+ },
87
+ configFilePath: configFilePath,
88
+
89
+ // Microsoft Graph API
90
+ GRAPH_API_ENDPOINT: 'https://graph.microsoft.com/v1.0/',
91
+
92
+ // Calendar constants
93
+ CALENDAR_SELECT_FIELDS: 'id,subject,start,end,location,bodyPreview,isAllDay,recurrence,attendees',
94
+
95
+ // Email constants
96
+ EMAIL_SELECT_FIELDS: 'id,subject,from,toRecipients,ccRecipients,receivedDateTime,bodyPreview,hasAttachments,importance,isRead',
97
+ EMAIL_DETAIL_FIELDS: 'id,subject,from,toRecipients,ccRecipients,bccRecipients,receivedDateTime,bodyPreview,body,hasAttachments,importance,isRead,internetMessageHeaders',
98
+
99
+ // Calendar constants
100
+ CALENDAR_SELECT_FIELDS: 'id,subject,bodyPreview,start,end,location,organizer,attendees,isAllDay,isCancelled',
101
+
102
+ // Pagination
103
+ DEFAULT_PAGE_SIZE: 25,
104
+ MAX_RESULT_COUNT: 50,
105
+
106
+ // Timezone
107
+ DEFAULT_TIMEZONE: "Central European Standard Time",
108
+ };
@@ -0,0 +1,175 @@
1
+ /**
2
+ * Email folder utilities
3
+ */
4
+ const { callGraphAPI } = require('../utils/graph-api');
5
+
6
+ /**
7
+ * Cache of folder information to reduce API calls
8
+ * Format: { userId: { folderName: { id, path } } }
9
+ */
10
+ const folderCache = {};
11
+
12
+ /**
13
+ * Well-known folder names and their endpoints
14
+ */
15
+ const WELL_KNOWN_FOLDERS = {
16
+ 'inbox': 'me/mailFolders/inbox/messages',
17
+ 'drafts': 'me/mailFolders/drafts/messages',
18
+ 'sent': 'me/mailFolders/sentItems/messages',
19
+ 'deleted': 'me/mailFolders/deletedItems/messages',
20
+ 'junk': 'me/mailFolders/junkemail/messages',
21
+ 'archive': 'me/mailFolders/archive/messages'
22
+ };
23
+
24
+ /**
25
+ * Resolve a folder name to its endpoint path
26
+ * @param {string} accessToken - Access token
27
+ * @param {string} folderName - Folder name to resolve
28
+ * @returns {Promise<string>} - Resolved endpoint path
29
+ */
30
+ async function resolveFolderPath(accessToken, folderName) {
31
+
32
+ // Default to inbox if no folder specified
33
+ if (!folderName) {
34
+ return WELL_KNOWN_FOLDERS['inbox'];
35
+ }
36
+
37
+ // Check if it's a well-known folder (case-insensitive)
38
+ const lowerFolderName = folderName.toLowerCase();
39
+ if (WELL_KNOWN_FOLDERS[lowerFolderName]) {
40
+ console.error(`Using well-known folder path for "${folderName}"`);
41
+ return WELL_KNOWN_FOLDERS[lowerFolderName];
42
+ }
43
+
44
+ try {
45
+ // Try to find the folder by name
46
+ const folderId = await getFolderIdByName(accessToken, folderName);
47
+ if (folderId) {
48
+ const path = `me/mailFolders/${folderId}/messages`;
49
+ console.error(`Resolved folder "${folderName}" to path: ${path}`);
50
+ return path;
51
+ }
52
+
53
+ // If not found, fall back to inbox
54
+ console.error(`Couldn't find folder "${folderName}", falling back to inbox`);
55
+ return WELL_KNOWN_FOLDERS['inbox'];
56
+ } catch (error) {
57
+ console.error(`Error resolving folder "${folderName}": ${error.message}`);
58
+ return WELL_KNOWN_FOLDERS['inbox'];
59
+ }
60
+ }
61
+
62
+ /**
63
+ * Get the ID of a mail folder by its name
64
+ * @param {string} accessToken - Access token
65
+ * @param {string} folderName - Name of the folder to find
66
+ * @returns {Promise<string|null>} - Folder ID or null if not found
67
+ */
68
+ async function getFolderIdByName(accessToken, folderName) {
69
+ try {
70
+ // First try with exact match filter
71
+ console.error(`Looking for folder with name "${folderName}"`);
72
+ const response = await callGraphAPI(
73
+ accessToken,
74
+ 'GET',
75
+ 'me/mailFolders',
76
+ null,
77
+ { $filter: `displayName eq '${folderName}'` }
78
+ );
79
+
80
+ if (response.value && response.value.length > 0) {
81
+ console.error(`Found folder "${folderName}" with ID: ${response.value[0].id}`);
82
+ return response.value[0].id;
83
+ }
84
+
85
+ // If exact match fails, try to get all folders and do a case-insensitive comparison
86
+ console.error(`No exact match found for "${folderName}", trying case-insensitive search`);
87
+ const allFoldersResponse = await callGraphAPI(
88
+ accessToken,
89
+ 'GET',
90
+ 'me/mailFolders',
91
+ null,
92
+ { $top: 100 }
93
+ );
94
+
95
+ if (allFoldersResponse.value) {
96
+ const lowerFolderName = folderName.toLowerCase();
97
+ const matchingFolder = allFoldersResponse.value.find(
98
+ folder => folder.displayName.toLowerCase() === lowerFolderName
99
+ );
100
+
101
+ if (matchingFolder) {
102
+ console.error(`Found case-insensitive match for "${folderName}" with ID: ${matchingFolder.id}`);
103
+ return matchingFolder.id;
104
+ }
105
+ }
106
+
107
+ console.error(`No folder found matching "${folderName}"`);
108
+ return null;
109
+ } catch (error) {
110
+ console.error(`Error finding folder "${folderName}": ${error.message}`);
111
+ return null;
112
+ }
113
+ }
114
+
115
+ /**
116
+ * Get all mail folders
117
+ * @param {string} accessToken - Access token
118
+ * @returns {Promise<Array>} - Array of folder objects
119
+ */
120
+ async function getAllFolders(accessToken) {
121
+ try {
122
+ // Get top-level folders
123
+ const response = await callGraphAPI(
124
+ accessToken,
125
+ 'GET',
126
+ 'me/mailFolders',
127
+ null,
128
+ {
129
+ $top: 100,
130
+ $select: 'id,displayName,parentFolderId,childFolderCount,totalItemCount,unreadItemCount'
131
+ }
132
+ );
133
+
134
+ if (!response.value) {
135
+ return [];
136
+ }
137
+
138
+ // Get child folders for folders with children
139
+ const foldersWithChildren = response.value.filter(f => f.childFolderCount > 0);
140
+
141
+ const childFolderPromises = foldersWithChildren.map(async (folder) => {
142
+ try {
143
+ const childResponse = await callGraphAPI(
144
+ accessToken,
145
+ 'GET',
146
+ `me/mailFolders/${folder.id}/childFolders`,
147
+ null,
148
+ {
149
+ $select: 'id,displayName,parentFolderId,childFolderCount,totalItemCount,unreadItemCount'
150
+ }
151
+ );
152
+
153
+ return childResponse.value || [];
154
+ } catch (error) {
155
+ console.error(`Error getting child folders for "${folder.displayName}": ${error.message}`);
156
+ return [];
157
+ }
158
+ });
159
+
160
+ const childFolders = await Promise.all(childFolderPromises);
161
+
162
+ // Combine top-level folders and all child folders
163
+ return [...response.value, ...childFolders.flat()];
164
+ } catch (error) {
165
+ console.error(`Error getting all folders: ${error.message}`);
166
+ return [];
167
+ }
168
+ }
169
+
170
+ module.exports = {
171
+ WELL_KNOWN_FOLDERS,
172
+ resolveFolderPath,
173
+ getFolderIdByName,
174
+ getAllFolders
175
+ };
package/email/index.js ADDED
@@ -0,0 +1,157 @@
1
+ /**
2
+ * Email module for Outlook MCP server
3
+ */
4
+ const handleListEmails = require('./list');
5
+ const handleSearchEmails = require('./search');
6
+ const handleReadEmail = require('./read');
7
+ const handleSendEmail = require('./send');
8
+ const handleMarkAsRead = require('./mark-as-read');
9
+
10
+ // Email tool definitions
11
+ const emailTools = [
12
+ {
13
+ name: "list-emails",
14
+ description: "Lists recent emails from your inbox",
15
+ inputSchema: {
16
+ type: "object",
17
+ properties: {
18
+ folder: {
19
+ type: "string",
20
+ description: "Email folder to list (e.g., 'inbox', 'sent', 'drafts', default: 'inbox')"
21
+ },
22
+ count: {
23
+ type: "number",
24
+ description: "Number of emails to retrieve (default: 10, max: 50)"
25
+ }
26
+ },
27
+ required: []
28
+ },
29
+ handler: handleListEmails
30
+ },
31
+ {
32
+ name: "search-emails",
33
+ description: "Search for emails using various criteria",
34
+ inputSchema: {
35
+ type: "object",
36
+ properties: {
37
+ query: {
38
+ type: "string",
39
+ description: "Search query text to find in emails"
40
+ },
41
+ folder: {
42
+ type: "string",
43
+ description: "Email folder to search in (default: 'inbox')"
44
+ },
45
+ from: {
46
+ type: "string",
47
+ description: "Filter by sender email address or name"
48
+ },
49
+ to: {
50
+ type: "string",
51
+ description: "Filter by recipient email address or name"
52
+ },
53
+ subject: {
54
+ type: "string",
55
+ description: "Filter by email subject"
56
+ },
57
+ hasAttachments: {
58
+ type: "boolean",
59
+ description: "Filter to only emails with attachments"
60
+ },
61
+ unreadOnly: {
62
+ type: "boolean",
63
+ description: "Filter to only unread emails"
64
+ },
65
+ count: {
66
+ type: "number",
67
+ description: "Number of results to return (default: 10, max: 50)"
68
+ }
69
+ },
70
+ required: []
71
+ },
72
+ handler: handleSearchEmails
73
+ },
74
+ {
75
+ name: "read-email",
76
+ description: "Reads the content of a specific email",
77
+ inputSchema: {
78
+ type: "object",
79
+ properties: {
80
+ id: {
81
+ type: "string",
82
+ description: "ID of the email to read"
83
+ }
84
+ },
85
+ required: ["id"]
86
+ },
87
+ handler: handleReadEmail
88
+ },
89
+ {
90
+ name: "send-email",
91
+ description: "Composes and sends a new email",
92
+ inputSchema: {
93
+ type: "object",
94
+ properties: {
95
+ to: {
96
+ type: "string",
97
+ description: "Comma-separated list of recipient email addresses"
98
+ },
99
+ cc: {
100
+ type: "string",
101
+ description: "Comma-separated list of CC recipient email addresses"
102
+ },
103
+ bcc: {
104
+ type: "string",
105
+ description: "Comma-separated list of BCC recipient email addresses"
106
+ },
107
+ subject: {
108
+ type: "string",
109
+ description: "Email subject"
110
+ },
111
+ body: {
112
+ type: "string",
113
+ description: "Email body content (can be plain text or HTML)"
114
+ },
115
+ importance: {
116
+ type: "string",
117
+ description: "Email importance (normal, high, low)",
118
+ enum: ["normal", "high", "low"]
119
+ },
120
+ saveToSentItems: {
121
+ type: "boolean",
122
+ description: "Whether to save the email to sent items"
123
+ }
124
+ },
125
+ required: ["to", "subject", "body"]
126
+ },
127
+ handler: handleSendEmail
128
+ },
129
+ {
130
+ name: "mark-as-read",
131
+ description: "Marks an email as read or unread",
132
+ inputSchema: {
133
+ type: "object",
134
+ properties: {
135
+ id: {
136
+ type: "string",
137
+ description: "ID of the email to mark as read/unread"
138
+ },
139
+ isRead: {
140
+ type: "boolean",
141
+ description: "Whether to mark as read (true) or unread (false). Default: true"
142
+ }
143
+ },
144
+ required: ["id"]
145
+ },
146
+ handler: handleMarkAsRead
147
+ }
148
+ ];
149
+
150
+ module.exports = {
151
+ emailTools,
152
+ handleListEmails,
153
+ handleSearchEmails,
154
+ handleReadEmail,
155
+ handleSendEmail,
156
+ handleMarkAsRead
157
+ };
package/email/list.js ADDED
@@ -0,0 +1,78 @@
1
+ /**
2
+ * List emails functionality
3
+ */
4
+ const config = require('../config');
5
+ const { callGraphAPI, callGraphAPIPaginated } = require('../utils/graph-api');
6
+ const { ensureAuthenticated } = require('../auth');
7
+ const { resolveFolderPath } = require('./folder-utils');
8
+
9
+ /**
10
+ * List emails handler
11
+ * @param {object} args - Tool arguments
12
+ * @returns {object} - MCP response
13
+ */
14
+ async function handleListEmails(args) {
15
+ const folder = args.folder || "inbox";
16
+ const requestedCount = args.count || 10;
17
+
18
+ try {
19
+ // Get access token
20
+ const accessToken = await ensureAuthenticated();
21
+
22
+ // Resolve the folder path
23
+ const endpoint = await resolveFolderPath(accessToken, folder);
24
+
25
+ // Add query parameters
26
+ const queryParams = {
27
+ $top: Math.min(50, requestedCount), // Use 50 per page for efficiency
28
+ $orderby: 'receivedDateTime desc',
29
+ $select: config.EMAIL_SELECT_FIELDS
30
+ };
31
+
32
+ // Make API call with pagination support
33
+ const response = await callGraphAPIPaginated(accessToken, 'GET', endpoint, queryParams, requestedCount);
34
+
35
+ if (!response.value || response.value.length === 0) {
36
+ return {
37
+ content: [{
38
+ type: "text",
39
+ text: `No emails found in ${folder}.`
40
+ }]
41
+ };
42
+ }
43
+
44
+ // Format results
45
+ const emailList = response.value.map((email, index) => {
46
+ const sender = email.from ? email.from.emailAddress : { name: 'Unknown', address: 'unknown' };
47
+ const date = new Date(email.receivedDateTime).toLocaleString();
48
+ const readStatus = email.isRead ? '' : '[UNREAD] ';
49
+
50
+ return `${index + 1}. ${readStatus}${date} - From: ${sender.name} (${sender.address})\nSubject: ${email.subject}\nID: ${email.id}\n`;
51
+ }).join("\n");
52
+
53
+ return {
54
+ content: [{
55
+ type: "text",
56
+ text: `Found ${response.value.length} emails in ${folder}:\n\n${emailList}`
57
+ }]
58
+ };
59
+ } catch (error) {
60
+ if (error.message === 'Authentication required') {
61
+ return {
62
+ content: [{
63
+ type: "text",
64
+ text: "Authentication required. Please use the 'authenticate' tool first."
65
+ }]
66
+ };
67
+ }
68
+
69
+ return {
70
+ content: [{
71
+ type: "text",
72
+ text: `Error listing emails: ${error.message}`
73
+ }]
74
+ };
75
+ }
76
+ }
77
+
78
+ module.exports = handleListEmails;
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Mark email as read functionality
3
+ */
4
+ const config = require('../config');
5
+ const { callGraphAPI } = require('../utils/graph-api');
6
+ const { ensureAuthenticated } = require('../auth');
7
+
8
+ /**
9
+ * Mark email as read handler
10
+ * @param {object} args - Tool arguments
11
+ * @returns {object} - MCP response
12
+ */
13
+ async function handleMarkAsRead(args) {
14
+ const emailId = args.id;
15
+ const isRead = args.isRead !== undefined ? args.isRead : true; // Default to true
16
+
17
+ if (!emailId) {
18
+ return {
19
+ content: [{
20
+ type: "text",
21
+ text: "Email ID is required."
22
+ }]
23
+ };
24
+ }
25
+
26
+ try {
27
+ // Get access token
28
+ const accessToken = await ensureAuthenticated();
29
+
30
+ // Make API call to update email read status
31
+ const endpoint = `me/messages/${encodeURIComponent(emailId)}`;
32
+ const updateData = {
33
+ isRead: isRead
34
+ };
35
+
36
+ try {
37
+ const result = await callGraphAPI(accessToken, 'PATCH', endpoint, updateData);
38
+
39
+ const status = isRead ? 'read' : 'unread';
40
+
41
+ return {
42
+ content: [
43
+ {
44
+ type: "text",
45
+ text: `Email successfully marked as ${status}.`
46
+ }
47
+ ]
48
+ };
49
+ } catch (error) {
50
+ console.error(`Error marking email as ${isRead ? 'read' : 'unread'}: ${error.message}`);
51
+
52
+ // Improved error handling with more specific messages
53
+ if (error.message.includes("doesn't belong to the targeted mailbox")) {
54
+ return {
55
+ content: [
56
+ {
57
+ type: "text",
58
+ text: `The email ID seems invalid or doesn't belong to your mailbox. Please try with a different email ID.`
59
+ }
60
+ ]
61
+ };
62
+ } else if (error.message.includes("UNAUTHORIZED")) {
63
+ return {
64
+ content: [
65
+ {
66
+ type: "text",
67
+ text: "Authentication failed. Please re-authenticate and try again."
68
+ }
69
+ ]
70
+ };
71
+ } else {
72
+ return {
73
+ content: [
74
+ {
75
+ type: "text",
76
+ text: `Failed to mark email as ${isRead ? 'read' : 'unread'}: ${error.message}`
77
+ }
78
+ ]
79
+ };
80
+ }
81
+ }
82
+ } catch (error) {
83
+ if (error.message === 'Authentication required') {
84
+ return {
85
+ content: [{
86
+ type: "text",
87
+ text: "Authentication required. Please use the 'authenticate' tool first."
88
+ }]
89
+ };
90
+ }
91
+
92
+ return {
93
+ content: [{
94
+ type: "text",
95
+ text: `Error accessing email: ${error.message}`
96
+ }]
97
+ };
98
+ }
99
+ }
100
+
101
+ module.exports = handleMarkAsRead;