@littlebearapps/outlook-assistant 3.3.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/.env.example +22 -0
- package/LICENSE +21 -0
- package/README.md +422 -0
- package/advanced/index.js +652 -0
- package/auth/index.js +32 -0
- package/auth/oauth-server.js +233 -0
- package/auth/token-manager.js +105 -0
- package/auth/token-storage.js +359 -0
- package/auth/tools.js +159 -0
- package/calendar/accept.js +72 -0
- package/calendar/cancel.js +72 -0
- package/calendar/create.js +115 -0
- package/calendar/decline.js +72 -0
- package/calendar/delete.js +67 -0
- package/calendar/index.js +130 -0
- package/calendar/list.js +108 -0
- package/categories/index.js +955 -0
- package/config.js +95 -0
- package/contacts/index.js +754 -0
- package/email/attachments.js +365 -0
- package/email/conversations.js +666 -0
- package/email/delta.js +210 -0
- package/email/export.js +572 -0
- package/email/folder-utils.js +192 -0
- package/email/headers.js +344 -0
- package/email/index.js +537 -0
- package/email/list.js +136 -0
- package/email/mark-as-read.js +114 -0
- package/email/mime.js +286 -0
- package/email/read.js +161 -0
- package/email/search.js +628 -0
- package/email/send.js +169 -0
- package/folder/create.js +137 -0
- package/folder/delete.js +108 -0
- package/folder/index.js +112 -0
- package/folder/list.js +289 -0
- package/folder/move.js +186 -0
- package/folder/stats.js +322 -0
- package/index.js +162 -0
- package/llms.txt +76 -0
- package/outlook-auth-server.js +384 -0
- package/package.json +97 -0
- package/rules/create.js +273 -0
- package/rules/index.js +276 -0
- package/rules/list.js +216 -0
- package/settings/index.js +678 -0
- package/utils/field-presets.js +311 -0
- package/utils/graph-api.js +268 -0
- package/utils/mock-data.js +154 -0
- package/utils/odata-helpers.js +33 -0
- package/utils/response-formatter.js +457 -0
- package/utils/safety.js +123 -0
|
@@ -0,0 +1,754 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Contacts module for Outlook Assistant server
|
|
3
|
+
*
|
|
4
|
+
* Provides access to personal contacts and people search via Microsoft Graph API.
|
|
5
|
+
*/
|
|
6
|
+
const {
|
|
7
|
+
callGraphAPI,
|
|
8
|
+
callGraphAPIPaginated: _callGraphAPIPaginated,
|
|
9
|
+
} = require('../utils/graph-api');
|
|
10
|
+
const { ensureAuthenticated } = require('../auth');
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Contact field presets for different use cases
|
|
14
|
+
*/
|
|
15
|
+
const CONTACT_FIELDS = {
|
|
16
|
+
minimal: ['id', 'displayName', 'emailAddresses'],
|
|
17
|
+
list: [
|
|
18
|
+
'id',
|
|
19
|
+
'displayName',
|
|
20
|
+
'emailAddresses',
|
|
21
|
+
'mobilePhone',
|
|
22
|
+
'businessPhones',
|
|
23
|
+
],
|
|
24
|
+
full: [
|
|
25
|
+
'id',
|
|
26
|
+
'displayName',
|
|
27
|
+
'givenName',
|
|
28
|
+
'surname',
|
|
29
|
+
'emailAddresses',
|
|
30
|
+
'mobilePhone',
|
|
31
|
+
'businessPhones',
|
|
32
|
+
'homePhones',
|
|
33
|
+
'companyName',
|
|
34
|
+
'jobTitle',
|
|
35
|
+
'department',
|
|
36
|
+
'officeLocation',
|
|
37
|
+
'businessAddress',
|
|
38
|
+
'homeAddress',
|
|
39
|
+
'birthday',
|
|
40
|
+
'personalNotes',
|
|
41
|
+
'categories',
|
|
42
|
+
'createdDateTime',
|
|
43
|
+
'lastModifiedDateTime',
|
|
44
|
+
],
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Format contact for display
|
|
49
|
+
* @param {object} contact - Contact object from Graph API
|
|
50
|
+
* @param {string} verbosity - Output verbosity level
|
|
51
|
+
* @returns {string} - Formatted contact string
|
|
52
|
+
*/
|
|
53
|
+
function formatContact(contact, verbosity = 'standard') {
|
|
54
|
+
const lines = [];
|
|
55
|
+
|
|
56
|
+
lines.push(`### ${contact.displayName || '(No name)'}`);
|
|
57
|
+
|
|
58
|
+
// Email addresses
|
|
59
|
+
if (contact.emailAddresses?.length > 0) {
|
|
60
|
+
const emails = contact.emailAddresses.map((e) => e.address).join(', ');
|
|
61
|
+
lines.push(`**Email**: ${emails}`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Skip phone/company/ID at minimal verbosity
|
|
65
|
+
if (verbosity !== 'minimal') {
|
|
66
|
+
// Phone numbers
|
|
67
|
+
const phones = [];
|
|
68
|
+
if (contact.mobilePhone) phones.push(`Mobile: ${contact.mobilePhone}`);
|
|
69
|
+
if (contact.businessPhones?.length > 0)
|
|
70
|
+
phones.push(`Work: ${contact.businessPhones[0]}`);
|
|
71
|
+
if (contact.homePhones?.length > 0)
|
|
72
|
+
phones.push(`Home: ${contact.homePhones[0]}`);
|
|
73
|
+
if (phones.length > 0) {
|
|
74
|
+
lines.push(`**Phone**: ${phones.join(' | ')}`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Company info
|
|
78
|
+
if (contact.companyName || contact.jobTitle) {
|
|
79
|
+
const company = [contact.jobTitle, contact.companyName]
|
|
80
|
+
.filter(Boolean)
|
|
81
|
+
.join(' at ');
|
|
82
|
+
lines.push(`**Company**: ${company}`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Full verbosity extras
|
|
87
|
+
if (verbosity === 'full') {
|
|
88
|
+
if (contact.department) lines.push(`**Department**: ${contact.department}`);
|
|
89
|
+
if (contact.officeLocation)
|
|
90
|
+
lines.push(`**Office**: ${contact.officeLocation}`);
|
|
91
|
+
if (contact.birthday) lines.push(`**Birthday**: ${contact.birthday}`);
|
|
92
|
+
|
|
93
|
+
// Addresses
|
|
94
|
+
if (contact.businessAddress?.city) {
|
|
95
|
+
const addr = contact.businessAddress;
|
|
96
|
+
lines.push(
|
|
97
|
+
`**Business Address**: ${[addr.street, addr.city, addr.state, addr.postalCode].filter(Boolean).join(', ')}`
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
if (contact.homeAddress?.city) {
|
|
101
|
+
const addr = contact.homeAddress;
|
|
102
|
+
lines.push(
|
|
103
|
+
`**Home Address**: ${[addr.street, addr.city, addr.state, addr.postalCode].filter(Boolean).join(', ')}`
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (contact.personalNotes) {
|
|
108
|
+
lines.push(
|
|
109
|
+
`**Notes**: ${contact.personalNotes.substring(0, 200)}${contact.personalNotes.length > 200 ? '...' : ''}`
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (verbosity !== 'minimal') {
|
|
115
|
+
lines.push(`*ID: \`${contact.id}\`*`);
|
|
116
|
+
}
|
|
117
|
+
lines.push('');
|
|
118
|
+
|
|
119
|
+
return lines.join('\n');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* List contacts handler
|
|
124
|
+
*/
|
|
125
|
+
async function handleListContacts(args) {
|
|
126
|
+
const count = Math.min(args.count || 50, 100);
|
|
127
|
+
const verbosity = args.outputVerbosity || 'standard';
|
|
128
|
+
const folder = args.folder || null; // null = default contacts folder
|
|
129
|
+
|
|
130
|
+
try {
|
|
131
|
+
const accessToken = await ensureAuthenticated();
|
|
132
|
+
|
|
133
|
+
const fields =
|
|
134
|
+
verbosity === 'full'
|
|
135
|
+
? CONTACT_FIELDS.full
|
|
136
|
+
: verbosity === 'minimal'
|
|
137
|
+
? CONTACT_FIELDS.minimal
|
|
138
|
+
: CONTACT_FIELDS.list;
|
|
139
|
+
const endpoint = folder
|
|
140
|
+
? `me/contactFolders/${folder}/contacts`
|
|
141
|
+
: 'me/contacts';
|
|
142
|
+
|
|
143
|
+
const queryParams = {
|
|
144
|
+
$select: fields.join(','),
|
|
145
|
+
$top: count,
|
|
146
|
+
$orderby: 'displayName',
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const response = await callGraphAPI(
|
|
150
|
+
accessToken,
|
|
151
|
+
'GET',
|
|
152
|
+
endpoint,
|
|
153
|
+
null,
|
|
154
|
+
queryParams
|
|
155
|
+
);
|
|
156
|
+
const contacts = response.value || [];
|
|
157
|
+
|
|
158
|
+
let output = [];
|
|
159
|
+
output.push(`# Contacts\n`);
|
|
160
|
+
output.push(`**Total**: ${contacts.length}`);
|
|
161
|
+
output.push('');
|
|
162
|
+
|
|
163
|
+
contacts.forEach((contact) => {
|
|
164
|
+
output.push(formatContact(contact, verbosity));
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
content: [{ type: 'text', text: output.join('\n') }],
|
|
169
|
+
_meta: { count: contacts.length },
|
|
170
|
+
};
|
|
171
|
+
} catch (error) {
|
|
172
|
+
if (error.message === 'Authentication required') {
|
|
173
|
+
return {
|
|
174
|
+
content: [
|
|
175
|
+
{
|
|
176
|
+
type: 'text',
|
|
177
|
+
text: "Authentication required. Please use the 'auth' tool with action=authenticate first.",
|
|
178
|
+
},
|
|
179
|
+
],
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
return {
|
|
183
|
+
content: [
|
|
184
|
+
{ type: 'text', text: `Error listing contacts: ${error.message}` },
|
|
185
|
+
],
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Search contacts handler
|
|
192
|
+
*/
|
|
193
|
+
async function handleSearchContacts(args) {
|
|
194
|
+
const query = args.query;
|
|
195
|
+
const count = Math.min(args.count || 25, 50);
|
|
196
|
+
const verbosity = args.outputVerbosity || 'standard';
|
|
197
|
+
|
|
198
|
+
if (!query) {
|
|
199
|
+
return { content: [{ type: 'text', text: 'Search query is required.' }] };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
try {
|
|
203
|
+
const accessToken = await ensureAuthenticated();
|
|
204
|
+
|
|
205
|
+
const fields =
|
|
206
|
+
verbosity === 'full'
|
|
207
|
+
? CONTACT_FIELDS.full
|
|
208
|
+
: verbosity === 'minimal'
|
|
209
|
+
? CONTACT_FIELDS.minimal
|
|
210
|
+
: CONTACT_FIELDS.list;
|
|
211
|
+
const endpoint = 'me/contacts';
|
|
212
|
+
|
|
213
|
+
// Build filter for name — emailAddresses/any() lambda is unreliable on personal accounts
|
|
214
|
+
// and $orderby is incompatible with $filter on the contacts endpoint
|
|
215
|
+
const escapedQuery = query.replace(/'/g, "''");
|
|
216
|
+
const filter = `contains(displayName,'${escapedQuery}')`;
|
|
217
|
+
|
|
218
|
+
let response;
|
|
219
|
+
try {
|
|
220
|
+
const queryParams = {
|
|
221
|
+
$select: fields.join(','),
|
|
222
|
+
$filter: filter,
|
|
223
|
+
$top: count,
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
response = await callGraphAPI(
|
|
227
|
+
accessToken,
|
|
228
|
+
'GET',
|
|
229
|
+
endpoint,
|
|
230
|
+
null,
|
|
231
|
+
queryParams
|
|
232
|
+
);
|
|
233
|
+
} catch (filterError) {
|
|
234
|
+
// Fallback: fetch all contacts and filter client-side if $filter is unsupported
|
|
235
|
+
console.error(
|
|
236
|
+
`Contact $filter failed (${filterError.message}), falling back to client-side filter`
|
|
237
|
+
);
|
|
238
|
+
const allParams = {
|
|
239
|
+
$select: fields.join(','),
|
|
240
|
+
$top: 100,
|
|
241
|
+
$orderby: 'displayName',
|
|
242
|
+
};
|
|
243
|
+
const allResponse = await callGraphAPI(
|
|
244
|
+
accessToken,
|
|
245
|
+
'GET',
|
|
246
|
+
endpoint,
|
|
247
|
+
null,
|
|
248
|
+
allParams
|
|
249
|
+
);
|
|
250
|
+
const q = query.toLowerCase();
|
|
251
|
+
response = {
|
|
252
|
+
value: (allResponse.value || []).filter(
|
|
253
|
+
(c) =>
|
|
254
|
+
c.displayName?.toLowerCase().includes(q) ||
|
|
255
|
+
c.emailAddresses?.some((e) => e.address?.toLowerCase().includes(q))
|
|
256
|
+
),
|
|
257
|
+
};
|
|
258
|
+
response.value = response.value.slice(0, count);
|
|
259
|
+
}
|
|
260
|
+
const contacts = response.value || [];
|
|
261
|
+
|
|
262
|
+
let output = [];
|
|
263
|
+
output.push(`# Contact Search Results\n`);
|
|
264
|
+
output.push(`**Query**: "${query}"`);
|
|
265
|
+
output.push(`**Found**: ${contacts.length}`);
|
|
266
|
+
output.push('');
|
|
267
|
+
|
|
268
|
+
contacts.forEach((contact) => {
|
|
269
|
+
output.push(formatContact(contact, verbosity));
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
return {
|
|
273
|
+
content: [{ type: 'text', text: output.join('\n') }],
|
|
274
|
+
_meta: { query, count: contacts.length },
|
|
275
|
+
};
|
|
276
|
+
} catch (error) {
|
|
277
|
+
if (error.message === 'Authentication required') {
|
|
278
|
+
return {
|
|
279
|
+
content: [
|
|
280
|
+
{
|
|
281
|
+
type: 'text',
|
|
282
|
+
text: "Authentication required. Please use the 'auth' tool with action=authenticate first.",
|
|
283
|
+
},
|
|
284
|
+
],
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
return {
|
|
288
|
+
content: [
|
|
289
|
+
{ type: 'text', text: `Error searching contacts: ${error.message}` },
|
|
290
|
+
],
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Get contact handler
|
|
297
|
+
*/
|
|
298
|
+
async function handleGetContact(args) {
|
|
299
|
+
const contactId = args.id;
|
|
300
|
+
|
|
301
|
+
if (!contactId) {
|
|
302
|
+
return { content: [{ type: 'text', text: 'Contact ID is required.' }] };
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
try {
|
|
306
|
+
const accessToken = await ensureAuthenticated();
|
|
307
|
+
|
|
308
|
+
const endpoint = `me/contacts/${contactId}`;
|
|
309
|
+
const queryParams = {
|
|
310
|
+
$select: CONTACT_FIELDS.full.join(','),
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
const contact = await callGraphAPI(
|
|
314
|
+
accessToken,
|
|
315
|
+
'GET',
|
|
316
|
+
endpoint,
|
|
317
|
+
null,
|
|
318
|
+
queryParams
|
|
319
|
+
);
|
|
320
|
+
|
|
321
|
+
const output = formatContact(contact, 'full');
|
|
322
|
+
|
|
323
|
+
return {
|
|
324
|
+
content: [{ type: 'text', text: `# Contact Details\n\n${output}` }],
|
|
325
|
+
_meta: { contactId: contact.id },
|
|
326
|
+
};
|
|
327
|
+
} catch (error) {
|
|
328
|
+
if (error.message === 'Authentication required') {
|
|
329
|
+
return {
|
|
330
|
+
content: [
|
|
331
|
+
{
|
|
332
|
+
type: 'text',
|
|
333
|
+
text: "Authentication required. Please use the 'auth' tool with action=authenticate first.",
|
|
334
|
+
},
|
|
335
|
+
],
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
return {
|
|
339
|
+
content: [
|
|
340
|
+
{ type: 'text', text: `Error getting contact: ${error.message}` },
|
|
341
|
+
],
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Create contact handler
|
|
348
|
+
*/
|
|
349
|
+
async function handleCreateContact(args) {
|
|
350
|
+
const { displayName, email, mobilePhone, companyName, jobTitle, notes } =
|
|
351
|
+
args;
|
|
352
|
+
|
|
353
|
+
if (!displayName && !email) {
|
|
354
|
+
return {
|
|
355
|
+
content: [
|
|
356
|
+
{ type: 'text', text: 'At least displayName or email is required.' },
|
|
357
|
+
],
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
try {
|
|
362
|
+
const accessToken = await ensureAuthenticated();
|
|
363
|
+
|
|
364
|
+
const contactData = {};
|
|
365
|
+
|
|
366
|
+
if (displayName) contactData.displayName = displayName;
|
|
367
|
+
if (email) {
|
|
368
|
+
contactData.emailAddresses = [
|
|
369
|
+
{ address: email, name: displayName || email },
|
|
370
|
+
];
|
|
371
|
+
}
|
|
372
|
+
if (mobilePhone) contactData.mobilePhone = mobilePhone;
|
|
373
|
+
if (companyName) contactData.companyName = companyName;
|
|
374
|
+
if (jobTitle) contactData.jobTitle = jobTitle;
|
|
375
|
+
if (notes) contactData.personalNotes = notes;
|
|
376
|
+
|
|
377
|
+
// Parse name into given/surname if provided
|
|
378
|
+
if (displayName && displayName.includes(' ')) {
|
|
379
|
+
const parts = displayName.split(' ');
|
|
380
|
+
contactData.givenName = parts[0];
|
|
381
|
+
contactData.surname = parts.slice(1).join(' ');
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const contact = await callGraphAPI(
|
|
385
|
+
accessToken,
|
|
386
|
+
'POST',
|
|
387
|
+
'me/contacts',
|
|
388
|
+
contactData
|
|
389
|
+
);
|
|
390
|
+
|
|
391
|
+
return {
|
|
392
|
+
content: [
|
|
393
|
+
{
|
|
394
|
+
type: 'text',
|
|
395
|
+
text: `# Contact Created\n\n${formatContact(contact, 'full')}`,
|
|
396
|
+
},
|
|
397
|
+
],
|
|
398
|
+
_meta: { contactId: contact.id },
|
|
399
|
+
};
|
|
400
|
+
} catch (error) {
|
|
401
|
+
if (error.message === 'Authentication required') {
|
|
402
|
+
return {
|
|
403
|
+
content: [
|
|
404
|
+
{
|
|
405
|
+
type: 'text',
|
|
406
|
+
text: "Authentication required. Please use the 'auth' tool with action=authenticate first.",
|
|
407
|
+
},
|
|
408
|
+
],
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
return {
|
|
412
|
+
content: [
|
|
413
|
+
{ type: 'text', text: `Error creating contact: ${error.message}` },
|
|
414
|
+
],
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Update contact handler
|
|
421
|
+
*/
|
|
422
|
+
async function handleUpdateContact(args) {
|
|
423
|
+
const { id, displayName, email, mobilePhone, companyName, jobTitle, notes } =
|
|
424
|
+
args;
|
|
425
|
+
|
|
426
|
+
if (!id) {
|
|
427
|
+
return { content: [{ type: 'text', text: 'Contact ID is required.' }] };
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
try {
|
|
431
|
+
const accessToken = await ensureAuthenticated();
|
|
432
|
+
|
|
433
|
+
const contactData = {};
|
|
434
|
+
|
|
435
|
+
if (displayName !== undefined) {
|
|
436
|
+
contactData.displayName = displayName;
|
|
437
|
+
if (displayName && displayName.includes(' ')) {
|
|
438
|
+
const parts = displayName.split(' ');
|
|
439
|
+
contactData.givenName = parts[0];
|
|
440
|
+
contactData.surname = parts.slice(1).join(' ');
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
if (email !== undefined) {
|
|
444
|
+
contactData.emailAddresses = email ? [{ address: email }] : [];
|
|
445
|
+
}
|
|
446
|
+
if (mobilePhone !== undefined) contactData.mobilePhone = mobilePhone;
|
|
447
|
+
if (companyName !== undefined) contactData.companyName = companyName;
|
|
448
|
+
if (jobTitle !== undefined) contactData.jobTitle = jobTitle;
|
|
449
|
+
if (notes !== undefined) contactData.personalNotes = notes;
|
|
450
|
+
|
|
451
|
+
const endpoint = `me/contacts/${id}`;
|
|
452
|
+
const contact = await callGraphAPI(
|
|
453
|
+
accessToken,
|
|
454
|
+
'PATCH',
|
|
455
|
+
endpoint,
|
|
456
|
+
contactData
|
|
457
|
+
);
|
|
458
|
+
|
|
459
|
+
return {
|
|
460
|
+
content: [
|
|
461
|
+
{
|
|
462
|
+
type: 'text',
|
|
463
|
+
text: `# Contact Updated\n\n${formatContact(contact, 'full')}`,
|
|
464
|
+
},
|
|
465
|
+
],
|
|
466
|
+
_meta: { contactId: contact.id },
|
|
467
|
+
};
|
|
468
|
+
} catch (error) {
|
|
469
|
+
if (error.message === 'Authentication required') {
|
|
470
|
+
return {
|
|
471
|
+
content: [
|
|
472
|
+
{
|
|
473
|
+
type: 'text',
|
|
474
|
+
text: "Authentication required. Please use the 'auth' tool with action=authenticate first.",
|
|
475
|
+
},
|
|
476
|
+
],
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
return {
|
|
480
|
+
content: [
|
|
481
|
+
{ type: 'text', text: `Error updating contact: ${error.message}` },
|
|
482
|
+
],
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* Delete contact handler
|
|
489
|
+
*/
|
|
490
|
+
async function handleDeleteContact(args) {
|
|
491
|
+
const contactId = args.id;
|
|
492
|
+
|
|
493
|
+
if (!contactId) {
|
|
494
|
+
return { content: [{ type: 'text', text: 'Contact ID is required.' }] };
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
try {
|
|
498
|
+
const accessToken = await ensureAuthenticated();
|
|
499
|
+
|
|
500
|
+
const endpoint = `me/contacts/${contactId}`;
|
|
501
|
+
await callGraphAPI(accessToken, 'DELETE', endpoint);
|
|
502
|
+
|
|
503
|
+
return {
|
|
504
|
+
content: [
|
|
505
|
+
{
|
|
506
|
+
type: 'text',
|
|
507
|
+
text: `# Contact Deleted\n\nContact with ID \`${contactId}\` has been deleted.`,
|
|
508
|
+
},
|
|
509
|
+
],
|
|
510
|
+
_meta: { contactId, deleted: true },
|
|
511
|
+
};
|
|
512
|
+
} catch (error) {
|
|
513
|
+
if (error.message === 'Authentication required') {
|
|
514
|
+
return {
|
|
515
|
+
content: [
|
|
516
|
+
{
|
|
517
|
+
type: 'text',
|
|
518
|
+
text: "Authentication required. Please use the 'auth' tool with action=authenticate first.",
|
|
519
|
+
},
|
|
520
|
+
],
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
return {
|
|
524
|
+
content: [
|
|
525
|
+
{ type: 'text', text: `Error deleting contact: ${error.message}` },
|
|
526
|
+
],
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* Search people handler (relevance-based search via People API)
|
|
533
|
+
*/
|
|
534
|
+
async function handleSearchPeople(args) {
|
|
535
|
+
const query = args.query;
|
|
536
|
+
const count = Math.min(args.count || 25, 50);
|
|
537
|
+
|
|
538
|
+
if (!query) {
|
|
539
|
+
return { content: [{ type: 'text', text: 'Search query is required.' }] };
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
try {
|
|
543
|
+
const accessToken = await ensureAuthenticated();
|
|
544
|
+
|
|
545
|
+
const endpoint = 'me/people';
|
|
546
|
+
const queryParams = {
|
|
547
|
+
$search: `"${query}"`,
|
|
548
|
+
$top: count,
|
|
549
|
+
$select:
|
|
550
|
+
'id,displayName,scoredEmailAddresses,phones,companyName,jobTitle,department,userPrincipalName,personType',
|
|
551
|
+
};
|
|
552
|
+
|
|
553
|
+
const response = await callGraphAPI(
|
|
554
|
+
accessToken,
|
|
555
|
+
'GET',
|
|
556
|
+
endpoint,
|
|
557
|
+
null,
|
|
558
|
+
queryParams
|
|
559
|
+
);
|
|
560
|
+
const people = response.value || [];
|
|
561
|
+
|
|
562
|
+
let output = [];
|
|
563
|
+
output.push(`# People Search Results\n`);
|
|
564
|
+
output.push(`**Query**: "${query}"`);
|
|
565
|
+
output.push(`**Found**: ${people.length} (sorted by relevance)`);
|
|
566
|
+
output.push('');
|
|
567
|
+
|
|
568
|
+
people.forEach((person, index) => {
|
|
569
|
+
output.push(`### ${index + 1}. ${person.displayName || '(No name)'}`);
|
|
570
|
+
|
|
571
|
+
// Person type
|
|
572
|
+
const personType = person.personType?.class || 'Unknown';
|
|
573
|
+
output.push(`**Type**: ${personType}`);
|
|
574
|
+
|
|
575
|
+
// Email
|
|
576
|
+
if (person.scoredEmailAddresses?.length > 0) {
|
|
577
|
+
output.push(`**Email**: ${person.scoredEmailAddresses[0].address}`);
|
|
578
|
+
} else if (person.userPrincipalName) {
|
|
579
|
+
output.push(`**Email**: ${person.userPrincipalName}`);
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// Company info
|
|
583
|
+
if (person.companyName || person.jobTitle) {
|
|
584
|
+
const company = [person.jobTitle, person.companyName]
|
|
585
|
+
.filter(Boolean)
|
|
586
|
+
.join(' at ');
|
|
587
|
+
output.push(`**Position**: ${company}`);
|
|
588
|
+
}
|
|
589
|
+
if (person.department) {
|
|
590
|
+
output.push(`**Department**: ${person.department}`);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// Phone
|
|
594
|
+
if (person.phones?.length > 0) {
|
|
595
|
+
output.push(`**Phone**: ${person.phones[0].number}`);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
output.push('');
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
return {
|
|
602
|
+
content: [{ type: 'text', text: output.join('\n') }],
|
|
603
|
+
_meta: { query, count: people.length },
|
|
604
|
+
};
|
|
605
|
+
} catch (error) {
|
|
606
|
+
if (error.message === 'Authentication required') {
|
|
607
|
+
return {
|
|
608
|
+
content: [
|
|
609
|
+
{
|
|
610
|
+
type: 'text',
|
|
611
|
+
text: "Authentication required. Please use the 'auth' tool with action=authenticate first.",
|
|
612
|
+
},
|
|
613
|
+
],
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
return {
|
|
617
|
+
content: [
|
|
618
|
+
{ type: 'text', text: `Error searching people: ${error.message}` },
|
|
619
|
+
],
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// Consolidated tool definitions (7 → 2)
|
|
625
|
+
const contactsTools = [
|
|
626
|
+
{
|
|
627
|
+
name: 'manage-contact',
|
|
628
|
+
description:
|
|
629
|
+
'Manage personal contacts. action=list (default) lists contacts. action=search searches by name/email. action=get retrieves full details. action=create adds a contact. action=update modifies a contact. action=delete removes a contact.',
|
|
630
|
+
annotations: {
|
|
631
|
+
title: 'Contacts',
|
|
632
|
+
readOnlyHint: false,
|
|
633
|
+
destructiveHint: false,
|
|
634
|
+
openWorldHint: false,
|
|
635
|
+
},
|
|
636
|
+
inputSchema: {
|
|
637
|
+
type: 'object',
|
|
638
|
+
properties: {
|
|
639
|
+
action: {
|
|
640
|
+
type: 'string',
|
|
641
|
+
enum: ['list', 'search', 'get', 'create', 'update', 'delete'],
|
|
642
|
+
description: 'Action to perform (default: list)',
|
|
643
|
+
},
|
|
644
|
+
// list params
|
|
645
|
+
count: {
|
|
646
|
+
type: 'number',
|
|
647
|
+
description:
|
|
648
|
+
'Number of results (action=list default: 50, action=search default: 25)',
|
|
649
|
+
},
|
|
650
|
+
folder: {
|
|
651
|
+
type: 'string',
|
|
652
|
+
description: 'Contact folder ID (action=list)',
|
|
653
|
+
},
|
|
654
|
+
outputVerbosity: {
|
|
655
|
+
type: 'string',
|
|
656
|
+
enum: ['minimal', 'standard', 'full'],
|
|
657
|
+
description:
|
|
658
|
+
'Output detail level (action=list/search, default: standard)',
|
|
659
|
+
},
|
|
660
|
+
// search params
|
|
661
|
+
query: {
|
|
662
|
+
type: 'string',
|
|
663
|
+
description:
|
|
664
|
+
'Search query for name or email (action=search, required)',
|
|
665
|
+
},
|
|
666
|
+
// get/update/delete params
|
|
667
|
+
id: {
|
|
668
|
+
type: 'string',
|
|
669
|
+
description: 'Contact ID (action=get/update/delete, required)',
|
|
670
|
+
},
|
|
671
|
+
// create/update params
|
|
672
|
+
displayName: {
|
|
673
|
+
type: 'string',
|
|
674
|
+
description: 'Full name (action=create/update)',
|
|
675
|
+
},
|
|
676
|
+
email: {
|
|
677
|
+
type: 'string',
|
|
678
|
+
description: 'Primary email address (action=create/update)',
|
|
679
|
+
},
|
|
680
|
+
mobilePhone: {
|
|
681
|
+
type: 'string',
|
|
682
|
+
description: 'Mobile phone number (action=create/update)',
|
|
683
|
+
},
|
|
684
|
+
companyName: {
|
|
685
|
+
type: 'string',
|
|
686
|
+
description: 'Company name (action=create/update)',
|
|
687
|
+
},
|
|
688
|
+
jobTitle: {
|
|
689
|
+
type: 'string',
|
|
690
|
+
description: 'Job title (action=create/update)',
|
|
691
|
+
},
|
|
692
|
+
notes: {
|
|
693
|
+
type: 'string',
|
|
694
|
+
description: 'Personal notes (action=create/update)',
|
|
695
|
+
},
|
|
696
|
+
},
|
|
697
|
+
required: [],
|
|
698
|
+
},
|
|
699
|
+
handler: async (args) => {
|
|
700
|
+
const action = args.action || 'list';
|
|
701
|
+
switch (action) {
|
|
702
|
+
case 'search':
|
|
703
|
+
return handleSearchContacts(args);
|
|
704
|
+
case 'get':
|
|
705
|
+
return handleGetContact(args);
|
|
706
|
+
case 'create':
|
|
707
|
+
return handleCreateContact(args);
|
|
708
|
+
case 'update':
|
|
709
|
+
return handleUpdateContact(args);
|
|
710
|
+
case 'delete':
|
|
711
|
+
return handleDeleteContact(args);
|
|
712
|
+
case 'list':
|
|
713
|
+
default:
|
|
714
|
+
return handleListContacts(args);
|
|
715
|
+
}
|
|
716
|
+
},
|
|
717
|
+
},
|
|
718
|
+
{
|
|
719
|
+
name: 'search-people',
|
|
720
|
+
description:
|
|
721
|
+
'Search for people by relevance (includes contacts, directory, and recent communications). Uses a different API from manage-contact search.',
|
|
722
|
+
annotations: {
|
|
723
|
+
title: 'People Search',
|
|
724
|
+
readOnlyHint: true,
|
|
725
|
+
openWorldHint: false,
|
|
726
|
+
},
|
|
727
|
+
inputSchema: {
|
|
728
|
+
type: 'object',
|
|
729
|
+
properties: {
|
|
730
|
+
query: {
|
|
731
|
+
type: 'string',
|
|
732
|
+
description: 'Search query (name, email, company)',
|
|
733
|
+
},
|
|
734
|
+
count: {
|
|
735
|
+
type: 'number',
|
|
736
|
+
description: 'Maximum results to return (default: 25, max: 50)',
|
|
737
|
+
},
|
|
738
|
+
},
|
|
739
|
+
required: ['query'],
|
|
740
|
+
},
|
|
741
|
+
handler: handleSearchPeople,
|
|
742
|
+
},
|
|
743
|
+
];
|
|
744
|
+
|
|
745
|
+
module.exports = {
|
|
746
|
+
contactsTools,
|
|
747
|
+
handleListContacts,
|
|
748
|
+
handleSearchContacts,
|
|
749
|
+
handleGetContact,
|
|
750
|
+
handleCreateContact,
|
|
751
|
+
handleUpdateContact,
|
|
752
|
+
handleDeleteContact,
|
|
753
|
+
handleSearchPeople,
|
|
754
|
+
};
|