@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.
Files changed (52) hide show
  1. package/.env.example +22 -0
  2. package/LICENSE +21 -0
  3. package/README.md +422 -0
  4. package/advanced/index.js +652 -0
  5. package/auth/index.js +32 -0
  6. package/auth/oauth-server.js +233 -0
  7. package/auth/token-manager.js +105 -0
  8. package/auth/token-storage.js +359 -0
  9. package/auth/tools.js +159 -0
  10. package/calendar/accept.js +72 -0
  11. package/calendar/cancel.js +72 -0
  12. package/calendar/create.js +115 -0
  13. package/calendar/decline.js +72 -0
  14. package/calendar/delete.js +67 -0
  15. package/calendar/index.js +130 -0
  16. package/calendar/list.js +108 -0
  17. package/categories/index.js +955 -0
  18. package/config.js +95 -0
  19. package/contacts/index.js +754 -0
  20. package/email/attachments.js +365 -0
  21. package/email/conversations.js +666 -0
  22. package/email/delta.js +210 -0
  23. package/email/export.js +572 -0
  24. package/email/folder-utils.js +192 -0
  25. package/email/headers.js +344 -0
  26. package/email/index.js +537 -0
  27. package/email/list.js +136 -0
  28. package/email/mark-as-read.js +114 -0
  29. package/email/mime.js +286 -0
  30. package/email/read.js +161 -0
  31. package/email/search.js +628 -0
  32. package/email/send.js +169 -0
  33. package/folder/create.js +137 -0
  34. package/folder/delete.js +108 -0
  35. package/folder/index.js +112 -0
  36. package/folder/list.js +289 -0
  37. package/folder/move.js +186 -0
  38. package/folder/stats.js +322 -0
  39. package/index.js +162 -0
  40. package/llms.txt +76 -0
  41. package/outlook-auth-server.js +384 -0
  42. package/package.json +97 -0
  43. package/rules/create.js +273 -0
  44. package/rules/index.js +276 -0
  45. package/rules/list.js +216 -0
  46. package/settings/index.js +678 -0
  47. package/utils/field-presets.js +311 -0
  48. package/utils/graph-api.js +268 -0
  49. package/utils/mock-data.js +154 -0
  50. package/utils/odata-helpers.js +33 -0
  51. package/utils/response-formatter.js +457 -0
  52. 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
+ };