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,14 @@
1
+ /**
2
+ * Message retrieval functionality.
3
+ * Handles fetching and parsing message content.
4
+ */
5
+ import type { Page } from 'playwright';
6
+ import type { TeamsMessage } from '../types/teams.js';
7
+ /**
8
+ * Extracts messages from the current view (chat or channel).
9
+ */
10
+ export declare function getVisibleMessages(page: Page, maxMessages?: number): Promise<TeamsMessage[]>;
11
+ /**
12
+ * Scrolls to load more messages in the current view.
13
+ */
14
+ export declare function loadMoreMessages(page: Page): Promise<void>;
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Message retrieval functionality.
3
+ * Handles fetching and parsing message content.
4
+ */
5
+ // Message container selectors
6
+ const MESSAGE_SELECTORS = [
7
+ '[data-tid="chat-pane-message"]',
8
+ '[data-tid="message-container"]',
9
+ '.message-list-item',
10
+ '[role="listitem"][data-testid*="message"]',
11
+ ];
12
+ /**
13
+ * Extracts messages from the current view (chat or channel).
14
+ */
15
+ export async function getVisibleMessages(page, maxMessages = 50) {
16
+ const messages = [];
17
+ for (const selector of MESSAGE_SELECTORS) {
18
+ const elements = await page.locator(selector).all();
19
+ for (const element of elements.slice(0, maxMessages)) {
20
+ try {
21
+ const message = await parseMessageElement(element);
22
+ if (message) {
23
+ messages.push(message);
24
+ }
25
+ }
26
+ catch {
27
+ // Continue to next message
28
+ }
29
+ }
30
+ if (messages.length > 0)
31
+ break;
32
+ }
33
+ return messages;
34
+ }
35
+ /**
36
+ * Parses a message DOM element into a TeamsMessage object.
37
+ */
38
+ async function parseMessageElement(element) {
39
+ // Extract message ID
40
+ const id = await element.getAttribute('data-tid') ??
41
+ await element.getAttribute('id') ??
42
+ `msg-${Date.now()}`;
43
+ // Extract message content
44
+ const contentSelectors = [
45
+ '[data-tid="message-body"]',
46
+ '.message-body',
47
+ '[role="document"]',
48
+ 'p',
49
+ ];
50
+ let content = '';
51
+ for (const selector of contentSelectors) {
52
+ try {
53
+ const contentEl = element.locator(selector).first();
54
+ const text = await contentEl.textContent();
55
+ if (text) {
56
+ content = text.trim();
57
+ break;
58
+ }
59
+ }
60
+ catch {
61
+ continue;
62
+ }
63
+ }
64
+ if (!content) {
65
+ const fullText = await element.textContent();
66
+ content = fullText?.trim() ?? '';
67
+ }
68
+ if (!content) {
69
+ return null;
70
+ }
71
+ // Extract sender
72
+ const senderSelectors = [
73
+ '[data-tid="message-author"]',
74
+ '.message-author',
75
+ '[data-tid="sender-name"]',
76
+ ];
77
+ let sender = 'Unknown';
78
+ for (const selector of senderSelectors) {
79
+ try {
80
+ const senderEl = element.locator(selector).first();
81
+ const text = await senderEl.textContent();
82
+ if (text) {
83
+ sender = text.trim();
84
+ break;
85
+ }
86
+ }
87
+ catch {
88
+ continue;
89
+ }
90
+ }
91
+ // Extract timestamp
92
+ const timeSelectors = [
93
+ '[data-tid="message-timestamp"]',
94
+ 'time',
95
+ '[datetime]',
96
+ ];
97
+ let timestamp = new Date().toISOString();
98
+ for (const selector of timeSelectors) {
99
+ try {
100
+ const timeEl = element.locator(selector).first();
101
+ const datetime = await timeEl.getAttribute('datetime') ??
102
+ await timeEl.textContent();
103
+ if (datetime) {
104
+ timestamp = datetime.trim();
105
+ break;
106
+ }
107
+ }
108
+ catch {
109
+ continue;
110
+ }
111
+ }
112
+ return {
113
+ id,
114
+ content,
115
+ sender,
116
+ timestamp,
117
+ };
118
+ }
119
+ /**
120
+ * Scrolls to load more messages in the current view.
121
+ */
122
+ export async function loadMoreMessages(page) {
123
+ // Find the scrollable container
124
+ const scrollContainers = [
125
+ '[data-tid="message-list"]',
126
+ '.message-list',
127
+ '[role="main"]',
128
+ ];
129
+ for (const selector of scrollContainers) {
130
+ try {
131
+ const container = page.locator(selector).first();
132
+ await container.evaluate((el) => {
133
+ el.scrollTop = 0; // Scroll to top to load older messages
134
+ });
135
+ await page.waitForTimeout(1000);
136
+ return;
137
+ }
138
+ catch {
139
+ continue;
140
+ }
141
+ }
142
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Teams search functionality.
3
+ * Implements both API interception (preferred) and DOM scraping (fallback).
4
+ */
5
+ import type { Page } from 'playwright';
6
+ import type { TeamsSearchResult, TeamsSearchResultsWithPagination } from '../types/teams.js';
7
+ export interface SearchOptions {
8
+ /** Maximum results to return (for backward compat). Default: 25 */
9
+ maxResults?: number;
10
+ /** Timeout for waiting for results. Default: 10000 */
11
+ waitMs?: number;
12
+ /** Whether to use API interception (preferred). Default: true */
13
+ useApiInterception?: boolean;
14
+ /** Enable debug logging. Default: false */
15
+ debug?: boolean;
16
+ /** Starting offset for pagination (0-based). Default: 0 */
17
+ from?: number;
18
+ /** Page size. Default: 25 */
19
+ size?: number;
20
+ }
21
+ /**
22
+ * Main search function.
23
+ * Searches Teams for messages matching the query.
24
+ *
25
+ * Prefers API interception for structured results, falls back to DOM scraping.
26
+ */
27
+ export declare function searchTeams(page: Page, query: string, options?: SearchOptions): Promise<TeamsSearchResult[]>;
28
+ /**
29
+ * Main search function with pagination metadata.
30
+ * Searches Teams for messages matching the query and returns pagination info.
31
+ *
32
+ * The Substrate v2 query API uses from/size pagination:
33
+ * - from: Starting offset (0, 25, 50, 75, 100...)
34
+ * - size: Page size (default 25)
35
+ */
36
+ export declare function searchTeamsWithPagination(page: Page, query: string, options?: SearchOptions): Promise<TeamsSearchResultsWithPagination>;
37
+ /**
38
+ * Filters messages in the current view (channel/chat).
39
+ */
40
+ export declare function filterCurrentMessages(page: Page, query: string): Promise<TeamsSearchResult[]>;
@@ -0,0 +1,458 @@
1
+ /**
2
+ * Teams search functionality.
3
+ * Implements both API interception (preferred) and DOM scraping (fallback).
4
+ */
5
+ import { setupApiInterceptor } from './api-interceptor.js';
6
+ import { stripHtml } from '../utils/parsers.js';
7
+ // Search box selectors for Teams v2 web app
8
+ const SEARCH_SELECTORS = [
9
+ '[data-tid="searchInputField"]',
10
+ '[data-tid="app-search-input"]',
11
+ 'input[data-tid*="search"]',
12
+ 'input[placeholder*="Search" i]',
13
+ 'input[aria-label*="Search" i]',
14
+ 'input[type="search"]',
15
+ '[data-tid="search-box"]',
16
+ '[role="search"] input',
17
+ ];
18
+ // Clickable search trigger selectors
19
+ const SEARCH_TRIGGER_SELECTORS = [
20
+ '[data-tid="search-button"]',
21
+ '[data-tid="app-bar-search"]',
22
+ 'button[aria-label*="Search" i]',
23
+ '[aria-label*="Search" i][role="button"]',
24
+ ];
25
+ // DOM selectors from the Teams bookmarklet (proven to work)
26
+ // These target the chat/message view structure
27
+ const MESSAGE_SELECTORS = {
28
+ item: '[data-tid="chat-pane-item"]',
29
+ message: '[data-tid="chat-pane-message"]',
30
+ controlMessage: '[data-tid="control-message-renderer"]',
31
+ authorName: '[data-tid="message-author-name"]',
32
+ timestamp: '[id^="timestamp-"]',
33
+ time: 'time',
34
+ content: '[id^="content-"]:not([id^="content-control"])',
35
+ edited: '[id^="edited-"]',
36
+ reactions: '[data-tid="diverse-reaction-pill-button"]',
37
+ };
38
+ // Search result specific selectors
39
+ const SEARCH_RESULT_SELECTORS = [
40
+ '[data-tid*="search-result"]',
41
+ '[data-tid*="message-result"]',
42
+ '[data-tid*="searchResult"]',
43
+ '[data-tid*="result-item"]',
44
+ '[role="listitem"]:has([role="img"])',
45
+ '[role="option"]:has([role="img"])',
46
+ ];
47
+ const DEFAULT_OPTIONS = {
48
+ maxResults: 25,
49
+ waitMs: 10000,
50
+ useApiInterception: true,
51
+ debug: false,
52
+ from: 0,
53
+ size: 25,
54
+ };
55
+ /**
56
+ * Finds a working search input element on the page.
57
+ */
58
+ async function findSearchInput(page, debug = false) {
59
+ for (const selector of SEARCH_SELECTORS) {
60
+ try {
61
+ const locator = page.locator(selector).first();
62
+ const count = await locator.count();
63
+ if (count > 0) {
64
+ const isVisible = await locator.isVisible().catch(() => false);
65
+ if (isVisible) {
66
+ if (debug)
67
+ console.log(` [dom] Found search input: ${selector}`);
68
+ return locator;
69
+ }
70
+ }
71
+ }
72
+ catch {
73
+ continue;
74
+ }
75
+ }
76
+ return null;
77
+ }
78
+ /**
79
+ * Finds a clickable search trigger element.
80
+ */
81
+ async function findSearchTrigger(page, debug = false) {
82
+ for (const selector of SEARCH_TRIGGER_SELECTORS) {
83
+ try {
84
+ const locator = page.locator(selector).first();
85
+ const count = await locator.count();
86
+ if (count > 0) {
87
+ const isVisible = await locator.isVisible().catch(() => false);
88
+ if (isVisible) {
89
+ if (debug)
90
+ console.log(` [dom] Found search trigger: ${selector}`);
91
+ return locator;
92
+ }
93
+ }
94
+ }
95
+ catch {
96
+ continue;
97
+ }
98
+ }
99
+ return null;
100
+ }
101
+ /**
102
+ * Opens the search interface using various methods.
103
+ */
104
+ async function openSearch(page, debug = false) {
105
+ await page.waitForLoadState('domcontentloaded');
106
+ await page.waitForTimeout(2000);
107
+ // Check if search input is already visible
108
+ let searchInput = await findSearchInput(page, debug);
109
+ if (searchInput) {
110
+ return searchInput;
111
+ }
112
+ // Try keyboard shortcuts
113
+ const isMac = process.platform === 'darwin';
114
+ const shortcuts = [
115
+ isMac ? 'Meta+e' : 'Control+e',
116
+ isMac ? 'Meta+f' : 'Control+f',
117
+ 'F3',
118
+ ];
119
+ for (const shortcut of shortcuts) {
120
+ if (debug)
121
+ console.log(` [dom] Trying shortcut: ${shortcut}`);
122
+ await page.keyboard.press(shortcut);
123
+ await page.waitForTimeout(1000);
124
+ searchInput = await findSearchInput(page, debug);
125
+ if (searchInput) {
126
+ return searchInput;
127
+ }
128
+ await page.keyboard.press('Escape');
129
+ await page.waitForTimeout(300);
130
+ }
131
+ // Try clicking search trigger buttons
132
+ const searchTrigger = await findSearchTrigger(page, debug);
133
+ if (searchTrigger) {
134
+ if (debug)
135
+ console.log(' [dom] Clicking search trigger');
136
+ await searchTrigger.click();
137
+ await page.waitForTimeout(1000);
138
+ searchInput = await findSearchInput(page, debug);
139
+ if (searchInput) {
140
+ return searchInput;
141
+ }
142
+ }
143
+ throw new Error('Could not find search input. Teams UI may have changed. ' +
144
+ 'Run with debug:true to see what elements are available.');
145
+ }
146
+ /**
147
+ * Types a search query and submits it.
148
+ */
149
+ async function typeSearchQuery(page, searchInput, query, debug = false) {
150
+ if (debug)
151
+ console.log(` [dom] Typing query: "${query}"`);
152
+ await searchInput.scrollIntoViewIfNeeded().catch(() => { });
153
+ await page.waitForTimeout(300);
154
+ await searchInput.waitFor({ state: 'visible', timeout: 5000 }).catch(() => { });
155
+ // Try multiple interaction strategies
156
+ let typed = false;
157
+ // Strategy 1: Direct fill
158
+ try {
159
+ await searchInput.fill(query, { timeout: 5000 });
160
+ typed = true;
161
+ if (debug)
162
+ console.log(' [dom] Used fill() strategy');
163
+ }
164
+ catch (e) {
165
+ if (debug)
166
+ console.log(` [dom] fill() failed: ${e instanceof Error ? e.message : e}`);
167
+ }
168
+ // Strategy 2: Click then type
169
+ if (!typed) {
170
+ try {
171
+ await searchInput.click({ timeout: 5000 });
172
+ await page.waitForTimeout(200);
173
+ await page.keyboard.type(query, { delay: 30 });
174
+ typed = true;
175
+ if (debug)
176
+ console.log(' [dom] Used click+keyboard.type() strategy');
177
+ }
178
+ catch (e) {
179
+ if (debug)
180
+ console.log(` [dom] click+type failed: ${e instanceof Error ? e.message : e}`);
181
+ }
182
+ }
183
+ // Strategy 3: Focus via JavaScript
184
+ if (!typed) {
185
+ try {
186
+ await searchInput.evaluate((el) => {
187
+ el.focus();
188
+ el.value = '';
189
+ });
190
+ await page.keyboard.type(query, { delay: 30 });
191
+ typed = true;
192
+ if (debug)
193
+ console.log(' [dom] Used JS focus+type strategy');
194
+ }
195
+ catch (e) {
196
+ if (debug)
197
+ console.log(` [dom] JS focus+type failed: ${e instanceof Error ? e.message : e}`);
198
+ }
199
+ }
200
+ if (!typed) {
201
+ throw new Error('Failed to type into search input using all strategies');
202
+ }
203
+ await page.waitForTimeout(500);
204
+ await page.keyboard.press('Enter');
205
+ if (debug)
206
+ console.log(' [dom] Query submitted');
207
+ }
208
+ // stripHtml imported from ../utils/parsers.js
209
+ /**
210
+ * Extracts search results from the DOM using bookmarklet-inspired selectors.
211
+ * Fallback when API interception doesn't capture results.
212
+ */
213
+ async function extractResultsFromDom(page, maxResults, debug = false) {
214
+ const results = [];
215
+ const seenContent = new Set();
216
+ if (debug)
217
+ console.log(' [dom] Extracting results from DOM...');
218
+ // Strategy 1: Look for chat-pane-item elements (bookmarklet pattern)
219
+ const items = await page.locator(MESSAGE_SELECTORS.item).all();
220
+ if (debug)
221
+ console.log(` [dom] Found ${items.length} chat-pane-item elements`);
222
+ for (const item of items) {
223
+ if (results.length >= maxResults)
224
+ break;
225
+ try {
226
+ // Skip control/system messages
227
+ const isControl = await item.locator(MESSAGE_SELECTORS.controlMessage).count() > 0;
228
+ if (isControl)
229
+ continue;
230
+ // Check it's a message
231
+ const hasMessage = await item.locator(MESSAGE_SELECTORS.message).count() > 0;
232
+ if (!hasMessage)
233
+ continue;
234
+ // Extract sender
235
+ const sender = await item.locator(MESSAGE_SELECTORS.authorName).textContent().catch(() => null);
236
+ // Extract timestamp (try timestamp id first, then time element)
237
+ let timestamp;
238
+ const timestampEl = item.locator(MESSAGE_SELECTORS.timestamp).first();
239
+ if (await timestampEl.count() > 0) {
240
+ timestamp = await timestampEl.getAttribute('datetime').catch(() => null) || undefined;
241
+ }
242
+ if (!timestamp) {
243
+ const timeEl = item.locator(MESSAGE_SELECTORS.time).first();
244
+ if (await timeEl.count() > 0) {
245
+ timestamp = await timeEl.getAttribute('datetime').catch(() => null) || undefined;
246
+ }
247
+ }
248
+ // Extract content
249
+ const contentEl = item.locator(MESSAGE_SELECTORS.content).first();
250
+ let content = '';
251
+ if (await contentEl.count() > 0) {
252
+ const html = await contentEl.innerHTML().catch(() => null);
253
+ content = html ? stripHtml(html) : '';
254
+ }
255
+ // Skip empty or too short content
256
+ if (content.length < 5)
257
+ continue;
258
+ // Deduplicate
259
+ const key = content.substring(0, 60).toLowerCase();
260
+ if (seenContent.has(key))
261
+ continue;
262
+ seenContent.add(key);
263
+ results.push({
264
+ id: `dom-${results.length}`,
265
+ type: 'message',
266
+ content,
267
+ sender: sender?.trim() || undefined,
268
+ timestamp,
269
+ });
270
+ }
271
+ catch {
272
+ continue;
273
+ }
274
+ }
275
+ // Strategy 2: Try search result specific selectors
276
+ if (results.length < maxResults) {
277
+ for (const selector of SEARCH_RESULT_SELECTORS) {
278
+ const elements = await page.locator(selector).all();
279
+ if (debug && elements.length > 0) {
280
+ console.log(` [dom] Found ${elements.length} elements with: ${selector}`);
281
+ }
282
+ for (const element of elements) {
283
+ if (results.length >= maxResults)
284
+ break;
285
+ try {
286
+ const text = await element.textContent();
287
+ if (!text || text.length < 20)
288
+ continue;
289
+ const cleaned = stripHtml(text).replace(/\s+/g, ' ').trim();
290
+ if (cleaned.length < 15)
291
+ continue;
292
+ // Deduplicate
293
+ const key = cleaned.substring(0, 60).toLowerCase();
294
+ if (seenContent.has(key))
295
+ continue;
296
+ seenContent.add(key);
297
+ // Try to parse sender and timestamp from text
298
+ const parsed = parseResultText(cleaned);
299
+ results.push({
300
+ id: `result-${results.length}`,
301
+ type: 'message',
302
+ content: parsed.content,
303
+ sender: parsed.sender,
304
+ timestamp: parsed.timestamp,
305
+ });
306
+ }
307
+ catch {
308
+ continue;
309
+ }
310
+ }
311
+ if (results.length >= maxResults)
312
+ break;
313
+ }
314
+ }
315
+ if (debug)
316
+ console.log(` [dom] Extracted ${results.length} results from DOM`);
317
+ return results;
318
+ }
319
+ /**
320
+ * Parses a result string to extract sender, timestamp, and content.
321
+ */
322
+ function parseResultText(text) {
323
+ let remaining = text;
324
+ // Pattern: "Lastname, Firstname" at the start
325
+ const senderMatch = remaining.match(/^([A-Z][a-zA-Z'-]+,\s*[A-Z][a-zA-Z'-]+)/);
326
+ let sender;
327
+ if (senderMatch) {
328
+ sender = senderMatch[1];
329
+ remaining = remaining.slice(sender.length).trim();
330
+ }
331
+ // Pattern: date/time
332
+ const timePatterns = [
333
+ /^(\d{1,2}\/\d{1,2}\/\d{4}\s+\d{1,2}:\d{2})/,
334
+ /^(Yesterday\s+\d{1,2}:\d{2})/i,
335
+ /^(Today\s+\d{1,2}:\d{2})/i,
336
+ /^(Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday)\s+(\d{1,2}:\d{2})/i,
337
+ ];
338
+ let timestamp;
339
+ for (const pattern of timePatterns) {
340
+ const match = remaining.match(pattern);
341
+ if (match) {
342
+ timestamp = match[0];
343
+ remaining = remaining.slice(timestamp.length).trim();
344
+ break;
345
+ }
346
+ }
347
+ return { sender, timestamp, content: remaining };
348
+ }
349
+ /**
350
+ * Main search function.
351
+ * Searches Teams for messages matching the query.
352
+ *
353
+ * Prefers API interception for structured results, falls back to DOM scraping.
354
+ */
355
+ export async function searchTeams(page, query, options = {}) {
356
+ const opts = { ...DEFAULT_OPTIONS, ...options };
357
+ const debug = opts.debug;
358
+ // Set up API interception before triggering search
359
+ const interceptor = opts.useApiInterception ? setupApiInterceptor(page, debug) : null;
360
+ try {
361
+ // Open search and type query
362
+ const searchInput = await openSearch(page, debug);
363
+ await typeSearchQuery(page, searchInput, query, debug);
364
+ // Wait for results - try API first, then DOM
365
+ if (interceptor) {
366
+ if (debug)
367
+ console.log(' [api] Waiting for API results...');
368
+ const apiResults = await interceptor.waitForSearchResults(opts.waitMs);
369
+ if (apiResults.length > 0) {
370
+ if (debug)
371
+ console.log(` [api] Got ${apiResults.length} results from API`);
372
+ return apiResults.slice(0, opts.maxResults);
373
+ }
374
+ if (debug)
375
+ console.log(' [api] No API results, falling back to DOM');
376
+ }
377
+ // Wait a bit for DOM to render
378
+ await page.waitForTimeout(2000);
379
+ // Fall back to DOM extraction
380
+ return extractResultsFromDom(page, opts.maxResults, debug);
381
+ }
382
+ finally {
383
+ // Clean up interceptor
384
+ interceptor?.stop();
385
+ }
386
+ }
387
+ /**
388
+ * Main search function with pagination metadata.
389
+ * Searches Teams for messages matching the query and returns pagination info.
390
+ *
391
+ * The Substrate v2 query API uses from/size pagination:
392
+ * - from: Starting offset (0, 25, 50, 75, 100...)
393
+ * - size: Page size (default 25)
394
+ */
395
+ export async function searchTeamsWithPagination(page, query, options = {}) {
396
+ const opts = { ...DEFAULT_OPTIONS, ...options };
397
+ const debug = opts.debug;
398
+ const defaultPagination = {
399
+ returned: 0,
400
+ from: opts.from,
401
+ size: opts.size,
402
+ hasMore: false,
403
+ };
404
+ // Set up API interception before triggering search
405
+ const interceptor = opts.useApiInterception ? setupApiInterceptor(page, debug) : null;
406
+ try {
407
+ // Open search and type query
408
+ const searchInput = await openSearch(page, debug);
409
+ await typeSearchQuery(page, searchInput, query, debug);
410
+ // Wait for results - try API first, then DOM
411
+ if (interceptor) {
412
+ if (debug)
413
+ console.log(' [api] Waiting for API results with pagination...');
414
+ const apiResults = await interceptor.waitForSearchResultsWithPagination(opts.waitMs);
415
+ if (apiResults.results.length > 0) {
416
+ if (debug) {
417
+ console.log(` [api] Got ${apiResults.results.length} results from API`);
418
+ console.log(` [api] Pagination: from=${apiResults.pagination.from}, size=${apiResults.pagination.size}, hasMore=${apiResults.pagination.hasMore}`);
419
+ }
420
+ return {
421
+ results: apiResults.results.slice(0, opts.maxResults),
422
+ pagination: {
423
+ returned: Math.min(apiResults.results.length, opts.maxResults),
424
+ from: apiResults.pagination.from,
425
+ size: apiResults.pagination.size,
426
+ total: apiResults.pagination.total,
427
+ hasMore: apiResults.pagination.hasMore,
428
+ },
429
+ };
430
+ }
431
+ if (debug)
432
+ console.log(' [api] No API results, falling back to DOM');
433
+ }
434
+ // Wait a bit for DOM to render
435
+ await page.waitForTimeout(2000);
436
+ // Fall back to DOM extraction
437
+ const domResults = await extractResultsFromDom(page, opts.maxResults, debug);
438
+ return {
439
+ results: domResults,
440
+ pagination: {
441
+ ...defaultPagination,
442
+ returned: domResults.length,
443
+ // For DOM extraction, we don't know the total, assume more if we hit maxResults
444
+ hasMore: domResults.length >= opts.maxResults,
445
+ },
446
+ };
447
+ }
448
+ finally {
449
+ // Clean up interceptor
450
+ interceptor?.stop();
451
+ }
452
+ }
453
+ /**
454
+ * Filters messages in the current view (channel/chat).
455
+ */
456
+ export async function filterCurrentMessages(page, query) {
457
+ return searchTeams(page, query);
458
+ }
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env npx tsx
2
+ /**
3
+ * CLI tool to interact with Teams MCP functionality directly.
4
+ * Useful for testing individual operations.
5
+ *
6
+ * Usage:
7
+ * npm run cli -- status
8
+ * npm run cli -- search "your query"
9
+ * npm run cli -- login
10
+ * npm run cli -- login --force
11
+ */
12
+ export {};