msteams-mcp 0.2.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 (80) hide show
  1. package/README.md +229 -0
  2. package/dist/__fixtures__/api-responses.d.ts +228 -0
  3. package/dist/__fixtures__/api-responses.js +217 -0
  4. package/dist/api/chatsvc-api.d.ts +171 -0
  5. package/dist/api/chatsvc-api.js +459 -0
  6. package/dist/api/csa-api.d.ts +44 -0
  7. package/dist/api/csa-api.js +148 -0
  8. package/dist/api/index.d.ts +6 -0
  9. package/dist/api/index.js +6 -0
  10. package/dist/api/substrate-api.d.ts +50 -0
  11. package/dist/api/substrate-api.js +305 -0
  12. package/dist/auth/crypto.d.ts +32 -0
  13. package/dist/auth/crypto.js +66 -0
  14. package/dist/auth/index.d.ts +6 -0
  15. package/dist/auth/index.js +6 -0
  16. package/dist/auth/session-store.d.ts +82 -0
  17. package/dist/auth/session-store.js +136 -0
  18. package/dist/auth/token-extractor.d.ts +69 -0
  19. package/dist/auth/token-extractor.js +330 -0
  20. package/dist/browser/auth.d.ts +43 -0
  21. package/dist/browser/auth.js +232 -0
  22. package/dist/browser/context.d.ts +40 -0
  23. package/dist/browser/context.js +121 -0
  24. package/dist/browser/session.d.ts +34 -0
  25. package/dist/browser/session.js +92 -0
  26. package/dist/constants.d.ts +54 -0
  27. package/dist/constants.js +72 -0
  28. package/dist/index.d.ts +8 -0
  29. package/dist/index.js +12 -0
  30. package/dist/research/explore.d.ts +11 -0
  31. package/dist/research/explore.js +267 -0
  32. package/dist/research/search-research.d.ts +17 -0
  33. package/dist/research/search-research.js +317 -0
  34. package/dist/server.d.ts +64 -0
  35. package/dist/server.js +291 -0
  36. package/dist/teams/api-interceptor.d.ts +54 -0
  37. package/dist/teams/api-interceptor.js +391 -0
  38. package/dist/teams/direct-api.d.ts +321 -0
  39. package/dist/teams/direct-api.js +1305 -0
  40. package/dist/teams/messages.d.ts +14 -0
  41. package/dist/teams/messages.js +142 -0
  42. package/dist/teams/search.d.ts +40 -0
  43. package/dist/teams/search.js +458 -0
  44. package/dist/test/cli.d.ts +12 -0
  45. package/dist/test/cli.js +328 -0
  46. package/dist/test/debug-search.d.ts +10 -0
  47. package/dist/test/debug-search.js +147 -0
  48. package/dist/test/manual-test.d.ts +11 -0
  49. package/dist/test/manual-test.js +160 -0
  50. package/dist/test/mcp-harness.d.ts +17 -0
  51. package/dist/test/mcp-harness.js +427 -0
  52. package/dist/tools/auth-tools.d.ts +26 -0
  53. package/dist/tools/auth-tools.js +127 -0
  54. package/dist/tools/index.d.ts +45 -0
  55. package/dist/tools/index.js +12 -0
  56. package/dist/tools/message-tools.d.ts +139 -0
  57. package/dist/tools/message-tools.js +433 -0
  58. package/dist/tools/people-tools.d.ts +46 -0
  59. package/dist/tools/people-tools.js +123 -0
  60. package/dist/tools/registry.d.ts +23 -0
  61. package/dist/tools/registry.js +61 -0
  62. package/dist/tools/search-tools.d.ts +79 -0
  63. package/dist/tools/search-tools.js +168 -0
  64. package/dist/types/errors.d.ts +58 -0
  65. package/dist/types/errors.js +132 -0
  66. package/dist/types/result.d.ts +43 -0
  67. package/dist/types/result.js +51 -0
  68. package/dist/types/teams.d.ts +79 -0
  69. package/dist/types/teams.js +5 -0
  70. package/dist/utils/api-config.d.ts +66 -0
  71. package/dist/utils/api-config.js +113 -0
  72. package/dist/utils/auth-guards.d.ts +29 -0
  73. package/dist/utils/auth-guards.js +54 -0
  74. package/dist/utils/http.d.ts +29 -0
  75. package/dist/utils/http.js +111 -0
  76. package/dist/utils/parsers.d.ts +187 -0
  77. package/dist/utils/parsers.js +574 -0
  78. package/dist/utils/parsers.test.d.ts +7 -0
  79. package/dist/utils/parsers.test.js +360 -0
  80. package/package.json +58 -0
@@ -0,0 +1,360 @@
1
+ /**
2
+ * Unit tests for parsing functions.
3
+ *
4
+ * Tests outcomes, not implementations - verify that given inputs
5
+ * produce expected outputs regardless of internal logic.
6
+ */
7
+ import { describe, it, expect } from 'vitest';
8
+ import { stripHtml, buildMessageLink, extractMessageTimestamp, parsePersonSuggestion, parseV2Result, parseJwtProfile, calculateTokenStatus, parseSearchResults, parsePeopleResults, extractObjectId, buildOneOnOneConversationId, decodeBase64Guid, } from './parsers.js';
9
+ import { searchResultItem, searchResultWithHtml, searchResultMinimal, searchResultTooShort, searchEntitySetsResponse, personSuggestion, personMinimal, personWithBase64Id, peopleGroupsResponse, jwtPayloadFull, jwtPayloadMinimal, jwtPayloadCommaName, jwtPayloadSpaceName, sourceWithMessageId, sourceWithConvIdMessageId, } from '../__fixtures__/api-responses.js';
10
+ describe('stripHtml', () => {
11
+ it('removes HTML tags', () => {
12
+ expect(stripHtml('<p>Hello</p>')).toBe('Hello');
13
+ expect(stripHtml('<div><strong>Bold</strong> text</div>')).toBe('Bold text');
14
+ });
15
+ it('decodes HTML entities', () => {
16
+ expect(stripHtml('Tom &amp; Jerry')).toBe('Tom & Jerry');
17
+ expect(stripHtml('1 &lt; 2 &gt; 0')).toBe('1 < 2 > 0');
18
+ expect(stripHtml('&quot;quoted&quot;')).toBe('"quoted"');
19
+ expect(stripHtml("it&#39;s")).toBe("it's");
20
+ expect(stripHtml('non&nbsp;breaking')).toBe('non breaking');
21
+ });
22
+ it('collapses whitespace', () => {
23
+ expect(stripHtml('hello world')).toBe('hello world');
24
+ expect(stripHtml(' trimmed ')).toBe('trimmed');
25
+ expect(stripHtml('line\n\nbreak')).toBe('line break');
26
+ });
27
+ it('handles complex HTML', () => {
28
+ const html = '<p>Meeting <strong>notes</strong> from &amp; yesterday&apos;s call</p><br/><div>Action items:</div>';
29
+ expect(stripHtml(html)).toBe("Meeting notes from & yesterday's call Action items:");
30
+ });
31
+ it('returns empty string for empty input', () => {
32
+ expect(stripHtml('')).toBe('');
33
+ });
34
+ });
35
+ describe('buildMessageLink', () => {
36
+ it('builds correct Teams deep link', () => {
37
+ const link = buildMessageLink('19:abc@thread.tacv2', '1705760000000');
38
+ expect(link).toBe('https://teams.microsoft.com/l/message/19%3Aabc%40thread.tacv2/1705760000000');
39
+ });
40
+ it('accepts numeric timestamp', () => {
41
+ const link = buildMessageLink('19:abc@thread.v2', 1705760000000);
42
+ expect(link).toBe('https://teams.microsoft.com/l/message/19%3Aabc%40thread.v2/1705760000000');
43
+ });
44
+ it('encodes special characters in conversation ID', () => {
45
+ const link = buildMessageLink('19:special@thread', '123');
46
+ expect(link).toContain('19%3Aspecial%40thread');
47
+ });
48
+ });
49
+ describe('extractMessageTimestamp', () => {
50
+ it('extracts from MessageId field', () => {
51
+ expect(extractMessageTimestamp(sourceWithMessageId)).toBe('1705760000000');
52
+ });
53
+ it('extracts from ClientConversationId suffix', () => {
54
+ expect(extractMessageTimestamp(sourceWithConvIdMessageId)).toBe('1705770000000');
55
+ });
56
+ it('falls back to parsing ISO timestamp', () => {
57
+ const timestamp = extractMessageTimestamp(undefined, '2026-01-20T12:00:00.000Z');
58
+ expect(timestamp).toBe(String(new Date('2026-01-20T12:00:00.000Z').getTime()));
59
+ });
60
+ it('returns undefined for missing data', () => {
61
+ expect(extractMessageTimestamp(undefined)).toBeUndefined();
62
+ expect(extractMessageTimestamp({})).toBeUndefined();
63
+ });
64
+ it('ignores invalid timestamp formats', () => {
65
+ expect(extractMessageTimestamp(undefined, 'not-a-date')).toBeUndefined();
66
+ });
67
+ });
68
+ describe('decodeBase64Guid', () => {
69
+ it('decodes base64-encoded GUID correctly', () => {
70
+ // '93qkaTtFGWpUHjyRafgdhg==' is a real base64-encoded GUID
71
+ const result = decodeBase64Guid('93qkaTtFGWpUHjyRafgdhg==');
72
+ expect(result).toBe('69a47af7-453b-6a19-541e-3c9169f81d86');
73
+ });
74
+ it('returns null for invalid base64', () => {
75
+ expect(decodeBase64Guid('not-valid-base64!')).toBeNull();
76
+ });
77
+ it('returns null for wrong length', () => {
78
+ // Too short (only 8 bytes when decoded)
79
+ expect(decodeBase64Guid('AAAAAAAAAAA=')).toBeNull();
80
+ // Too long (24 bytes when decoded)
81
+ expect(decodeBase64Guid('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==')).toBeNull();
82
+ });
83
+ it('returns lowercase GUID', () => {
84
+ const result = decodeBase64Guid('93qkaTtFGWpUHjyRafgdhg==');
85
+ expect(result).toBe(result?.toLowerCase());
86
+ });
87
+ });
88
+ describe('parsePersonSuggestion', () => {
89
+ it('parses complete person data', () => {
90
+ const result = parsePersonSuggestion(personSuggestion);
91
+ expect(result).not.toBeNull();
92
+ expect(result.id).toBe('a1b2c3d4-e5f6-7890-abcd-ef1234567890');
93
+ expect(result.mri).toBe('8:orgid:a1b2c3d4-e5f6-7890-abcd-ef1234567890');
94
+ expect(result.displayName).toBe('Smith, John');
95
+ expect(result.givenName).toBe('John');
96
+ expect(result.surname).toBe('Smith');
97
+ expect(result.email).toBe('john.smith@company.com');
98
+ expect(result.department).toBe('Engineering');
99
+ expect(result.jobTitle).toBe('Senior Engineer');
100
+ expect(result.companyName).toBe('Acme Corp');
101
+ });
102
+ it('handles minimal person data with GUID format', () => {
103
+ const result = parsePersonSuggestion(personMinimal);
104
+ expect(result).not.toBeNull();
105
+ expect(result.id).toBe('b1c2d3e4-f5a6-7890-bcde-1234567890ab');
106
+ expect(result.mri).toBe('8:orgid:b1c2d3e4-f5a6-7890-bcde-1234567890ab');
107
+ expect(result.displayName).toBe('Jane Doe');
108
+ expect(result.email).toBeUndefined();
109
+ });
110
+ it('decodes base64-encoded IDs', () => {
111
+ const result = parsePersonSuggestion(personWithBase64Id);
112
+ expect(result).not.toBeNull();
113
+ expect(result.id).toBe('69a47af7-453b-6a19-541e-3c9169f81d86');
114
+ expect(result.mri).toBe('8:orgid:69a47af7-453b-6a19-541e-3c9169f81d86');
115
+ expect(result.displayName).toBe('Rob MacDonald');
116
+ expect(result.email).toBe('rob@company.com');
117
+ });
118
+ it('extracts ID from tenant-qualified GUID format', () => {
119
+ const result = parsePersonSuggestion({
120
+ Id: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890@tenant.onmicrosoft.com',
121
+ DisplayName: 'Test User',
122
+ });
123
+ expect(result.id).toBe('a1b2c3d4-e5f6-7890-abcd-ef1234567890');
124
+ });
125
+ it('returns null for missing ID', () => {
126
+ expect(parsePersonSuggestion({ DisplayName: 'No ID' })).toBeNull();
127
+ });
128
+ it('returns null for invalid ID format', () => {
129
+ expect(parsePersonSuggestion({ Id: 'invalid-format', DisplayName: 'Test' })).toBeNull();
130
+ });
131
+ });
132
+ describe('parseV2Result', () => {
133
+ it('parses complete search result', () => {
134
+ const result = parseV2Result(searchResultItem);
135
+ expect(result).not.toBeNull();
136
+ expect(result.type).toBe('message');
137
+ expect(result.content).toBe('Let me check the budget report for Q3');
138
+ expect(result.timestamp).toBe('2026-01-20T14:30:00.000Z');
139
+ expect(result.channelName).toBe('General');
140
+ expect(result.teamName).toBe('Finance Team');
141
+ expect(result.conversationId).toBe('19:abcdef123456@thread.tacv2');
142
+ expect(result.messageLink).toContain('teams.microsoft.com/l/message');
143
+ });
144
+ it('strips HTML from content', () => {
145
+ const result = parseV2Result(searchResultWithHtml);
146
+ expect(result).not.toBeNull();
147
+ expect(result.content).toBe("Meeting notes from & yesterday's call Action items:");
148
+ expect(result.content).not.toContain('<');
149
+ expect(result.content).not.toContain('>');
150
+ });
151
+ it('handles minimal result', () => {
152
+ const result = parseV2Result(searchResultMinimal);
153
+ expect(result).not.toBeNull();
154
+ expect(result.id).toBe('minimal-id');
155
+ expect(result.content).toBe('A short message here');
156
+ expect(result.conversationId).toBeUndefined();
157
+ expect(result.messageLink).toBeUndefined();
158
+ });
159
+ it('returns null for content too short', () => {
160
+ expect(parseV2Result(searchResultTooShort)).toBeNull();
161
+ });
162
+ it('extracts conversationId from Extensions', () => {
163
+ const result = parseV2Result(searchResultItem);
164
+ expect(result.conversationId).toBe('19:abcdef123456@thread.tacv2');
165
+ });
166
+ it('falls back to ClientThreadId for conversationId', () => {
167
+ const result = parseV2Result(searchResultWithHtml);
168
+ expect(result.conversationId).toBe('19:meeting123@thread.v2');
169
+ });
170
+ });
171
+ describe('parseJwtProfile', () => {
172
+ it('parses complete JWT payload', () => {
173
+ const profile = parseJwtProfile(jwtPayloadFull);
174
+ expect(profile).not.toBeNull();
175
+ expect(profile.id).toBe('user-object-id-guid');
176
+ expect(profile.mri).toBe('8:orgid:user-object-id-guid');
177
+ expect(profile.email).toBe('rob.macdonald@company.com');
178
+ expect(profile.displayName).toBe('Macdonald, Rob');
179
+ expect(profile.givenName).toBe('Rob');
180
+ expect(profile.surname).toBe('Macdonald');
181
+ expect(profile.tenantId).toBe('tenant-id-guid');
182
+ });
183
+ it('handles minimal JWT payload', () => {
184
+ const profile = parseJwtProfile(jwtPayloadMinimal);
185
+ expect(profile).not.toBeNull();
186
+ expect(profile.id).toBe('another-user-guid');
187
+ expect(profile.displayName).toBe('Alice Smith');
188
+ expect(profile.email).toBe('');
189
+ // Should parse from "Alice Smith" format
190
+ expect(profile.givenName).toBe('Alice');
191
+ expect(profile.surname).toBe('Smith');
192
+ });
193
+ it('parses "Surname, GivenName" format', () => {
194
+ const profile = parseJwtProfile(jwtPayloadCommaName);
195
+ expect(profile.surname).toBe('Jones');
196
+ expect(profile.givenName).toBe('David');
197
+ });
198
+ it('parses "GivenName Surname" format', () => {
199
+ const profile = parseJwtProfile(jwtPayloadSpaceName);
200
+ expect(profile.givenName).toBe('Sarah');
201
+ expect(profile.surname).toBe('Connor');
202
+ });
203
+ it('returns null for missing required fields', () => {
204
+ expect(parseJwtProfile({})).toBeNull();
205
+ expect(parseJwtProfile({ oid: 'id-only' })).toBeNull();
206
+ expect(parseJwtProfile({ name: 'name-only' })).toBeNull();
207
+ });
208
+ it('prefers upn over other email fields', () => {
209
+ const profile = parseJwtProfile(jwtPayloadFull);
210
+ expect(profile.email).toBe('rob.macdonald@company.com');
211
+ });
212
+ });
213
+ describe('calculateTokenStatus', () => {
214
+ const now = 1705846400000; // Fixed "now" for testing
215
+ it('returns valid for unexpired token', () => {
216
+ const expiry = now + 3600000; // 1 hour from now
217
+ const status = calculateTokenStatus(expiry, now);
218
+ expect(status.isValid).toBe(true);
219
+ expect(status.minutesRemaining).toBe(60);
220
+ });
221
+ it('returns invalid for expired token', () => {
222
+ const expiry = now - 60000; // 1 minute ago
223
+ const status = calculateTokenStatus(expiry, now);
224
+ expect(status.isValid).toBe(false);
225
+ expect(status.minutesRemaining).toBe(0);
226
+ });
227
+ it('returns correct ISO date string', () => {
228
+ const expiry = now + 3600000;
229
+ const status = calculateTokenStatus(expiry, now);
230
+ expect(status.expiresAt).toBe(new Date(expiry).toISOString());
231
+ });
232
+ it('rounds minutes correctly', () => {
233
+ const status = calculateTokenStatus(now + 90000, now); // 1.5 minutes
234
+ expect(status.minutesRemaining).toBe(2); // Rounds up
235
+ });
236
+ });
237
+ describe('parseSearchResults', () => {
238
+ it('parses EntitySets structure', () => {
239
+ const { results, total } = parseSearchResults(searchEntitySetsResponse.EntitySets, 0, 25);
240
+ expect(results).toHaveLength(2);
241
+ expect(total).toBe(4307);
242
+ });
243
+ it('returns empty for undefined input', () => {
244
+ const { results, total } = parseSearchResults(undefined, 0, 25);
245
+ expect(results).toHaveLength(0);
246
+ expect(total).toBeUndefined();
247
+ });
248
+ it('returns empty for non-array input', () => {
249
+ const { results } = parseSearchResults('not an array', 0, 25);
250
+ expect(results).toHaveLength(0);
251
+ });
252
+ it('filters out results with short content', () => {
253
+ const entitySets = [{
254
+ ResultSets: [{
255
+ Results: [
256
+ { Id: '1', HitHighlightedSummary: 'Valid content here' },
257
+ { Id: '2', HitHighlightedSummary: 'Hi' }, // Too short
258
+ ],
259
+ }],
260
+ }];
261
+ const { results } = parseSearchResults(entitySets, 0, 25);
262
+ expect(results).toHaveLength(1);
263
+ });
264
+ });
265
+ describe('parsePeopleResults', () => {
266
+ it('parses Groups/Suggestions structure', () => {
267
+ const results = parsePeopleResults(peopleGroupsResponse.Groups);
268
+ expect(results).toHaveLength(2);
269
+ expect(results[0].displayName).toBe('Smith, John');
270
+ expect(results[1].displayName).toBe('Jane Doe');
271
+ });
272
+ it('returns empty for undefined input', () => {
273
+ expect(parsePeopleResults(undefined)).toHaveLength(0);
274
+ });
275
+ it('returns empty for non-array input', () => {
276
+ expect(parsePeopleResults('not an array')).toHaveLength(0);
277
+ });
278
+ it('handles groups with no suggestions', () => {
279
+ const groups = [{ Suggestions: [] }, { OtherField: 'value' }];
280
+ expect(parsePeopleResults(groups)).toHaveLength(0);
281
+ });
282
+ });
283
+ describe('extractObjectId', () => {
284
+ it('extracts GUID from MRI format', () => {
285
+ expect(extractObjectId('8:orgid:ab76f827-27e2-4c67-a765-f1a53145fa24'))
286
+ .toBe('ab76f827-27e2-4c67-a765-f1a53145fa24');
287
+ });
288
+ it('extracts GUID from Skype ID format (without 8: prefix)', () => {
289
+ expect(extractObjectId('orgid:ab76f827-27e2-4c67-a765-f1a53145fa24'))
290
+ .toBe('ab76f827-27e2-4c67-a765-f1a53145fa24');
291
+ });
292
+ it('extracts GUID from ID with tenant format', () => {
293
+ expect(extractObjectId('5817f485-f870-46eb-bbc4-de216babac62@56b731a8-a2ac-4c32-bf6b-616810e913c6'))
294
+ .toBe('5817f485-f870-46eb-bbc4-de216babac62');
295
+ });
296
+ it('returns raw GUID unchanged', () => {
297
+ expect(extractObjectId('ab76f827-27e2-4c67-a765-f1a53145fa24'))
298
+ .toBe('ab76f827-27e2-4c67-a765-f1a53145fa24');
299
+ });
300
+ it('normalises to lowercase', () => {
301
+ expect(extractObjectId('AB76F827-27E2-4C67-A765-F1A53145FA24'))
302
+ .toBe('ab76f827-27e2-4c67-a765-f1a53145fa24');
303
+ });
304
+ it('decodes base64-encoded GUID', () => {
305
+ expect(extractObjectId('93qkaTtFGWpUHjyRafgdhg=='))
306
+ .toBe('69a47af7-453b-6a19-541e-3c9169f81d86');
307
+ });
308
+ it('decodes base64 GUID from MRI format', () => {
309
+ expect(extractObjectId('8:orgid:93qkaTtFGWpUHjyRafgdhg=='))
310
+ .toBe('69a47af7-453b-6a19-541e-3c9169f81d86');
311
+ });
312
+ it('decodes base64 GUID from Skype ID format', () => {
313
+ expect(extractObjectId('orgid:93qkaTtFGWpUHjyRafgdhg=='))
314
+ .toBe('69a47af7-453b-6a19-541e-3c9169f81d86');
315
+ });
316
+ it('returns null for invalid formats', () => {
317
+ expect(extractObjectId('')).toBeNull();
318
+ expect(extractObjectId('not-a-guid')).toBeNull();
319
+ expect(extractObjectId('8:orgid:invalid')).toBeNull();
320
+ expect(extractObjectId('orgid:invalid')).toBeNull();
321
+ expect(extractObjectId('missing-sections-1234')).toBeNull();
322
+ });
323
+ });
324
+ describe('buildOneOnOneConversationId', () => {
325
+ const userId1 = 'ab76f827-27e2-4c67-a765-f1a53145fa24';
326
+ const userId2 = '5817f485-f870-46eb-bbc4-de216babac62';
327
+ it('builds conversation ID with sorted user IDs', () => {
328
+ // userId2 ('5817...') comes before userId1 ('ab76...') alphabetically
329
+ const result = buildOneOnOneConversationId(userId1, userId2);
330
+ expect(result).toBe('19:5817f485-f870-46eb-bbc4-de216babac62_ab76f827-27e2-4c67-a765-f1a53145fa24@unq.gbl.spaces');
331
+ });
332
+ it('produces same result regardless of argument order', () => {
333
+ const result1 = buildOneOnOneConversationId(userId1, userId2);
334
+ const result2 = buildOneOnOneConversationId(userId2, userId1);
335
+ expect(result1).toBe(result2);
336
+ });
337
+ it('handles MRI format input', () => {
338
+ const mri1 = `8:orgid:${userId1}`;
339
+ const mri2 = `8:orgid:${userId2}`;
340
+ const result = buildOneOnOneConversationId(mri1, mri2);
341
+ expect(result).toBe('19:5817f485-f870-46eb-bbc4-de216babac62_ab76f827-27e2-4c67-a765-f1a53145fa24@unq.gbl.spaces');
342
+ });
343
+ it('handles ID with tenant format', () => {
344
+ const idWithTenant = '5817f485-f870-46eb-bbc4-de216babac62@56b731a8-a2ac-4c32-bf6b-616810e913c6';
345
+ const result = buildOneOnOneConversationId(userId1, idWithTenant);
346
+ expect(result).toBe('19:5817f485-f870-46eb-bbc4-de216babac62_ab76f827-27e2-4c67-a765-f1a53145fa24@unq.gbl.spaces');
347
+ });
348
+ it('handles base64-encoded GUID input', () => {
349
+ // '93qkaTtFGWpUHjyRafgdhg==' decodes to '69a47af7-453b-6a19-541e-3c9169f81d86'
350
+ const base64Id = '93qkaTtFGWpUHjyRafgdhg==';
351
+ const result = buildOneOnOneConversationId(base64Id, userId2);
352
+ // '5817...' < '69a4...' so 5817 comes first
353
+ expect(result).toBe('19:5817f485-f870-46eb-bbc4-de216babac62_69a47af7-453b-6a19-541e-3c9169f81d86@unq.gbl.spaces');
354
+ });
355
+ it('returns null for invalid input', () => {
356
+ expect(buildOneOnOneConversationId('invalid', userId2)).toBeNull();
357
+ expect(buildOneOnOneConversationId(userId1, 'invalid')).toBeNull();
358
+ expect(buildOneOnOneConversationId('', '')).toBeNull();
359
+ });
360
+ });
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "msteams-mcp",
3
+ "version": "0.2.0",
4
+ "description": "MCP server for Microsoft Teams - search messages, send replies, manage favourites",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "bin": {
8
+ "msteams-mcp": "dist/index.js"
9
+ },
10
+ "files": [
11
+ "dist",
12
+ "README.md"
13
+ ],
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/m0nkmaster/microsoft-teams-mcp.git"
17
+ },
18
+ "homepage": "https://github.com/m0nkmaster/microsoft-teams-mcp#readme",
19
+ "bugs": {
20
+ "url": "https://github.com/m0nkmaster/microsoft-teams-mcp/issues"
21
+ },
22
+ "engines": {
23
+ "node": ">=18"
24
+ },
25
+ "scripts": {
26
+ "build": "tsc",
27
+ "start": "node dist/index.js",
28
+ "dev": "tsx src/index.ts",
29
+ "research": "tsx src/research/explore.ts",
30
+ "research:search": "tsx src/research/search-research.ts",
31
+ "test": "vitest run",
32
+ "test:watch": "vitest",
33
+ "test:coverage": "vitest run --coverage",
34
+ "test:mcp": "tsx src/test/mcp-harness.ts",
35
+ "cli": "tsx src/test/cli.ts",
36
+ "debug:search": "tsx src/test/debug-search.ts",
37
+ "typecheck": "tsc --noEmit"
38
+ },
39
+ "keywords": [
40
+ "mcp",
41
+ "microsoft-teams",
42
+ "playwright",
43
+ "browser-automation"
44
+ ],
45
+ "author": "Rob MacDonald",
46
+ "license": "MIT",
47
+ "dependencies": {
48
+ "@modelcontextprotocol/sdk": "^1.0.0",
49
+ "playwright": "^1.40.0",
50
+ "zod": "^3.22.0"
51
+ },
52
+ "devDependencies": {
53
+ "@types/node": "^20.10.0",
54
+ "tsx": "^4.7.0",
55
+ "typescript": "^5.3.0",
56
+ "vitest": "^4.0.18"
57
+ }
58
+ }