@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/README.md +396 -0
- package/auth/index.js +64 -0
- package/auth/oauth-server.js +178 -0
- package/auth/token-manager.js +139 -0
- package/auth/token-storage.js +317 -0
- package/auth/tools.js +171 -0
- package/calendar/accept.js +64 -0
- package/calendar/cancel.js +64 -0
- package/calendar/create.js +69 -0
- package/calendar/decline.js +64 -0
- package/calendar/delete.js +59 -0
- package/calendar/index.js +123 -0
- package/calendar/list.js +77 -0
- package/cli.js +246 -0
- package/config.js +108 -0
- package/email/folder-utils.js +175 -0
- package/email/index.js +157 -0
- package/email/list.js +78 -0
- package/email/mark-as-read.js +101 -0
- package/email/read.js +128 -0
- package/email/search.js +285 -0
- package/email/send.js +120 -0
- package/folder/create.js +124 -0
- package/folder/index.js +78 -0
- package/folder/list.js +264 -0
- package/folder/move.js +163 -0
- package/index.js +148 -0
- package/package.json +54 -0
- package/rules/create.js +248 -0
- package/rules/index.js +175 -0
- package/rules/list.js +202 -0
- package/utils/graph-api.js +192 -0
- package/utils/mock-data.js +145 -0
- package/utils/odata-helpers.js +40 -0
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;
|