@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
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Microsoft Graph API helper functions
|
|
3
|
+
*/
|
|
4
|
+
const https = require('https');
|
|
5
|
+
const config = require('../config');
|
|
6
|
+
const mockData = require('./mock-data');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Makes a request to the Microsoft Graph API
|
|
10
|
+
* @param {string} accessToken - The access token for authentication
|
|
11
|
+
* @param {string} method - HTTP method (GET, POST, etc.)
|
|
12
|
+
* @param {string} path - API endpoint path
|
|
13
|
+
* @param {object} data - Data to send for POST/PUT requests
|
|
14
|
+
* @param {object} queryParams - Query parameters
|
|
15
|
+
* @returns {Promise<object>} - The API response
|
|
16
|
+
*/
|
|
17
|
+
async function callGraphAPI(accessToken, method, path, data = null, queryParams = {}) {
|
|
18
|
+
// For test tokens, we'll simulate the API call
|
|
19
|
+
if (config.USE_TEST_MODE && accessToken.startsWith('test_access_token_')) {
|
|
20
|
+
console.error(`TEST MODE: Simulating ${method} ${path} API call`);
|
|
21
|
+
return mockData.simulateGraphAPIResponse(method, path, data, queryParams);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
console.error(`Making real API call: ${method} ${path}`);
|
|
26
|
+
|
|
27
|
+
// Check if path already contains the full URL (from nextLink)
|
|
28
|
+
let finalUrl;
|
|
29
|
+
if (path.startsWith('http://') || path.startsWith('https://')) {
|
|
30
|
+
// Path is already a full URL (from pagination nextLink)
|
|
31
|
+
finalUrl = path;
|
|
32
|
+
console.error(`Using full URL from nextLink: ${finalUrl}`);
|
|
33
|
+
} else {
|
|
34
|
+
// Build URL from path and queryParams
|
|
35
|
+
// Encode path segments properly
|
|
36
|
+
const encodedPath = path.split('/')
|
|
37
|
+
.map(segment => encodeURIComponent(segment))
|
|
38
|
+
.join('/');
|
|
39
|
+
|
|
40
|
+
// Build query string from parameters with special handling for OData filters
|
|
41
|
+
let queryString = '';
|
|
42
|
+
if (Object.keys(queryParams).length > 0) {
|
|
43
|
+
// Handle $filter parameter specially to ensure proper URI encoding
|
|
44
|
+
const filter = queryParams.$filter;
|
|
45
|
+
if (filter) {
|
|
46
|
+
delete queryParams.$filter; // Remove from regular params
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Build query string with proper encoding for regular params
|
|
50
|
+
const params = new URLSearchParams();
|
|
51
|
+
for (const [key, value] of Object.entries(queryParams)) {
|
|
52
|
+
params.append(key, value);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
queryString = params.toString();
|
|
56
|
+
|
|
57
|
+
// Add filter parameter separately with proper encoding
|
|
58
|
+
if (filter) {
|
|
59
|
+
if (queryString) {
|
|
60
|
+
queryString += `&$filter=${encodeURIComponent(filter)}`;
|
|
61
|
+
} else {
|
|
62
|
+
queryString = `$filter=${encodeURIComponent(filter)}`;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (queryString) {
|
|
67
|
+
queryString = '?' + queryString;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
console.error(`Query string: ${queryString}`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
finalUrl = `${config.GRAPH_API_ENDPOINT}${encodedPath}${queryString}`;
|
|
74
|
+
console.error(`Full URL: ${finalUrl}`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return new Promise((resolve, reject) => {
|
|
78
|
+
const options = {
|
|
79
|
+
method: method,
|
|
80
|
+
headers: {
|
|
81
|
+
'Authorization': `Bearer ${accessToken}`,
|
|
82
|
+
'Content-Type': 'application/json'
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const req = https.request(finalUrl, options, (res) => {
|
|
87
|
+
let responseData = '';
|
|
88
|
+
|
|
89
|
+
res.on('data', (chunk) => {
|
|
90
|
+
responseData += chunk;
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
res.on('end', () => {
|
|
94
|
+
if (res.statusCode >= 200 && res.statusCode < 300) {
|
|
95
|
+
try {
|
|
96
|
+
responseData = responseData ? responseData : '{}';
|
|
97
|
+
const jsonResponse = JSON.parse(responseData);
|
|
98
|
+
resolve(jsonResponse);
|
|
99
|
+
} catch (error) {
|
|
100
|
+
reject(new Error(`Error parsing API response: ${error.message}`));
|
|
101
|
+
}
|
|
102
|
+
} else if (res.statusCode === 401) {
|
|
103
|
+
// Token expired or invalid
|
|
104
|
+
reject(new Error('UNAUTHORIZED'));
|
|
105
|
+
} else {
|
|
106
|
+
reject(new Error(`API call failed with status ${res.statusCode}: ${responseData}`));
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
req.on('error', (error) => {
|
|
112
|
+
reject(new Error(`Network error during API call: ${error.message}`));
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
if (data && (method === 'POST' || method === 'PATCH' || method === 'PUT')) {
|
|
116
|
+
req.write(JSON.stringify(data));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
req.end();
|
|
120
|
+
});
|
|
121
|
+
} catch (error) {
|
|
122
|
+
console.error('Error calling Graph API:', error);
|
|
123
|
+
throw error;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Calls Graph API with pagination support to retrieve all results up to maxCount
|
|
129
|
+
* @param {string} accessToken - The access token for authentication
|
|
130
|
+
* @param {string} method - HTTP method (GET only for pagination)
|
|
131
|
+
* @param {string} path - API endpoint path
|
|
132
|
+
* @param {object} queryParams - Initial query parameters
|
|
133
|
+
* @param {number} maxCount - Maximum number of items to retrieve (0 = all)
|
|
134
|
+
* @returns {Promise<object>} - Combined API response with all items
|
|
135
|
+
*/
|
|
136
|
+
async function callGraphAPIPaginated(accessToken, method, path, queryParams = {}, maxCount = 0) {
|
|
137
|
+
if (method !== 'GET') {
|
|
138
|
+
throw new Error('Pagination only supports GET requests');
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const allItems = [];
|
|
142
|
+
let nextLink = null;
|
|
143
|
+
let currentUrl = path;
|
|
144
|
+
let currentParams = queryParams;
|
|
145
|
+
|
|
146
|
+
try {
|
|
147
|
+
do {
|
|
148
|
+
// Make API call
|
|
149
|
+
const response = await callGraphAPI(accessToken, method, currentUrl, null, currentParams);
|
|
150
|
+
|
|
151
|
+
// Add items from this page
|
|
152
|
+
if (response.value && Array.isArray(response.value)) {
|
|
153
|
+
allItems.push(...response.value);
|
|
154
|
+
console.error(`Pagination: Retrieved ${response.value.length} items, total so far: ${allItems.length}`);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Check if we've reached the desired count
|
|
158
|
+
if (maxCount > 0 && allItems.length >= maxCount) {
|
|
159
|
+
console.error(`Pagination: Reached max count of ${maxCount}, stopping`);
|
|
160
|
+
break;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Get next page URL
|
|
164
|
+
nextLink = response['@odata.nextLink'];
|
|
165
|
+
|
|
166
|
+
if (nextLink) {
|
|
167
|
+
// Pass the full nextLink URL directly to callGraphAPI
|
|
168
|
+
currentUrl = nextLink;
|
|
169
|
+
currentParams = {}; // nextLink already contains all params
|
|
170
|
+
console.error(`Pagination: Following nextLink, ${allItems.length} items so far`);
|
|
171
|
+
}
|
|
172
|
+
} while (nextLink);
|
|
173
|
+
|
|
174
|
+
// Trim to exact count if needed
|
|
175
|
+
const finalItems = maxCount > 0 ? allItems.slice(0, maxCount) : allItems;
|
|
176
|
+
|
|
177
|
+
console.error(`Pagination complete: Retrieved ${finalItems.length} total items`);
|
|
178
|
+
|
|
179
|
+
return {
|
|
180
|
+
value: finalItems,
|
|
181
|
+
'@odata.count': finalItems.length
|
|
182
|
+
};
|
|
183
|
+
} catch (error) {
|
|
184
|
+
console.error('Error during pagination:', error);
|
|
185
|
+
throw error;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
module.exports = {
|
|
190
|
+
callGraphAPI,
|
|
191
|
+
callGraphAPIPaginated
|
|
192
|
+
};
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mock data functions for test mode
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Simulates Microsoft Graph API responses for testing
|
|
7
|
+
* @param {string} method - HTTP method
|
|
8
|
+
* @param {string} path - API path
|
|
9
|
+
* @param {object} data - Request data
|
|
10
|
+
* @param {object} queryParams - Query parameters
|
|
11
|
+
* @returns {object} - Simulated API response
|
|
12
|
+
*/
|
|
13
|
+
function simulateGraphAPIResponse(method, path, data, queryParams) {
|
|
14
|
+
console.error(`Simulating response for: ${method} ${path}`);
|
|
15
|
+
|
|
16
|
+
if (method === 'GET') {
|
|
17
|
+
if (path.includes('messages') && !path.includes('sendMail')) {
|
|
18
|
+
// Simulate a successful email list/search response
|
|
19
|
+
if (path.includes('/messages/')) {
|
|
20
|
+
// Single email response
|
|
21
|
+
return {
|
|
22
|
+
id: "simulated-email-id",
|
|
23
|
+
subject: "Simulated Email Subject",
|
|
24
|
+
from: {
|
|
25
|
+
emailAddress: {
|
|
26
|
+
name: "Simulated Sender",
|
|
27
|
+
address: "sender@example.com"
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
toRecipients: [{
|
|
31
|
+
emailAddress: {
|
|
32
|
+
name: "Recipient Name",
|
|
33
|
+
address: "recipient@example.com"
|
|
34
|
+
}
|
|
35
|
+
}],
|
|
36
|
+
ccRecipients: [],
|
|
37
|
+
bccRecipients: [],
|
|
38
|
+
receivedDateTime: new Date().toISOString(),
|
|
39
|
+
bodyPreview: "This is a simulated email preview...",
|
|
40
|
+
body: {
|
|
41
|
+
contentType: "text",
|
|
42
|
+
content: "This is the full content of the simulated email. Since we can't connect to the real Microsoft Graph API, we're returning this placeholder content instead."
|
|
43
|
+
},
|
|
44
|
+
hasAttachments: false,
|
|
45
|
+
importance: "normal",
|
|
46
|
+
isRead: false,
|
|
47
|
+
internetMessageHeaders: []
|
|
48
|
+
};
|
|
49
|
+
} else {
|
|
50
|
+
// Email list response
|
|
51
|
+
return {
|
|
52
|
+
value: [
|
|
53
|
+
{
|
|
54
|
+
id: "simulated-email-1",
|
|
55
|
+
subject: "Important Meeting Tomorrow",
|
|
56
|
+
from: {
|
|
57
|
+
emailAddress: {
|
|
58
|
+
name: "John Doe",
|
|
59
|
+
address: "john@example.com"
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
toRecipients: [{
|
|
63
|
+
emailAddress: {
|
|
64
|
+
name: "You",
|
|
65
|
+
address: "you@example.com"
|
|
66
|
+
}
|
|
67
|
+
}],
|
|
68
|
+
ccRecipients: [],
|
|
69
|
+
receivedDateTime: new Date().toISOString(),
|
|
70
|
+
bodyPreview: "Let's discuss the project status...",
|
|
71
|
+
hasAttachments: false,
|
|
72
|
+
importance: "high",
|
|
73
|
+
isRead: false
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
id: "simulated-email-2",
|
|
77
|
+
subject: "Weekly Report",
|
|
78
|
+
from: {
|
|
79
|
+
emailAddress: {
|
|
80
|
+
name: "Jane Smith",
|
|
81
|
+
address: "jane@example.com"
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
toRecipients: [{
|
|
85
|
+
emailAddress: {
|
|
86
|
+
name: "You",
|
|
87
|
+
address: "you@example.com"
|
|
88
|
+
}
|
|
89
|
+
}],
|
|
90
|
+
ccRecipients: [],
|
|
91
|
+
receivedDateTime: new Date(Date.now() - 86400000).toISOString(), // Yesterday
|
|
92
|
+
bodyPreview: "Please find attached the weekly report...",
|
|
93
|
+
hasAttachments: true,
|
|
94
|
+
importance: "normal",
|
|
95
|
+
isRead: true
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
id: "simulated-email-3",
|
|
99
|
+
subject: "Question about the project",
|
|
100
|
+
from: {
|
|
101
|
+
emailAddress: {
|
|
102
|
+
name: "Bob Johnson",
|
|
103
|
+
address: "bob@example.com"
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
toRecipients: [{
|
|
107
|
+
emailAddress: {
|
|
108
|
+
name: "You",
|
|
109
|
+
address: "you@example.com"
|
|
110
|
+
}
|
|
111
|
+
}],
|
|
112
|
+
ccRecipients: [],
|
|
113
|
+
receivedDateTime: new Date(Date.now() - 172800000).toISOString(), // 2 days ago
|
|
114
|
+
bodyPreview: "I had a question about the timeline...",
|
|
115
|
+
hasAttachments: false,
|
|
116
|
+
importance: "normal",
|
|
117
|
+
isRead: false
|
|
118
|
+
}
|
|
119
|
+
]
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
} else if (path.includes('mailFolders')) {
|
|
123
|
+
// Simulate a mail folders response
|
|
124
|
+
return {
|
|
125
|
+
value: [
|
|
126
|
+
{ id: "inbox", displayName: "Inbox" },
|
|
127
|
+
{ id: "drafts", displayName: "Drafts" },
|
|
128
|
+
{ id: "sentItems", displayName: "Sent Items" },
|
|
129
|
+
{ id: "deleteditems", displayName: "Deleted Items" }
|
|
130
|
+
]
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
} else if (method === 'POST' && path.includes('sendMail')) {
|
|
134
|
+
// Simulate a successful email send
|
|
135
|
+
return {};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// If we get here, we don't have a simulation for this endpoint
|
|
139
|
+
console.error(`No simulation available for: ${method} ${path}`);
|
|
140
|
+
return {};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
module.exports = {
|
|
144
|
+
simulateGraphAPIResponse
|
|
145
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OData helper functions for Microsoft Graph API
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Escapes a string for use in OData queries
|
|
7
|
+
* @param {string} str - The string to escape
|
|
8
|
+
* @returns {string} - The escaped string
|
|
9
|
+
*/
|
|
10
|
+
function escapeODataString(str) {
|
|
11
|
+
if (!str) return str;
|
|
12
|
+
|
|
13
|
+
// Replace single quotes with double single quotes (OData escaping)
|
|
14
|
+
// And remove any special characters that could cause OData syntax errors
|
|
15
|
+
str = str.replace(/'/g, "''");
|
|
16
|
+
|
|
17
|
+
// Escape other potentially problematic characters
|
|
18
|
+
str = str.replace(/[\(\)\{\}\[\]\:\;\,\/\?\&\=\+\*\%\$\#\@\!\^]/g, '');
|
|
19
|
+
|
|
20
|
+
console.error(`Escaped OData string: '${str}'`);
|
|
21
|
+
return str;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Builds an OData filter from filter conditions
|
|
26
|
+
* @param {Array<string>} conditions - Array of filter conditions
|
|
27
|
+
* @returns {string} - Combined OData filter expression
|
|
28
|
+
*/
|
|
29
|
+
function buildODataFilter(conditions) {
|
|
30
|
+
if (!conditions || conditions.length === 0) {
|
|
31
|
+
return '';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return conditions.join(' and ');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
module.exports = {
|
|
38
|
+
escapeODataString,
|
|
39
|
+
buildODataFilter
|
|
40
|
+
};
|