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,328 @@
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
+ import { createBrowserContext, closeBrowser } from '../browser/context.js';
13
+ import { ensureAuthenticated, forceNewLogin } from '../browser/auth.js';
14
+ import { hasSessionState, getSessionAge, clearSessionState, } from '../auth/session-store.js';
15
+ import { hasValidSubstrateToken, getSubstrateTokenStatus, extractMessageAuth, getUserProfile, clearTokenCache, } from '../auth/token-extractor.js';
16
+ import { searchMessages } from '../api/substrate-api.js';
17
+ import { sendMessage, sendNoteToSelf } from '../api/chatsvc-api.js';
18
+ function parseArgs() {
19
+ const args = process.argv.slice(2);
20
+ const command = (args[0] ?? 'help');
21
+ const flags = new Set();
22
+ const options = new Map();
23
+ const remainingArgs = [];
24
+ for (let i = 1; i < args.length; i++) {
25
+ const arg = args[i];
26
+ if (arg.startsWith('--') && arg.includes('=')) {
27
+ const [key, value] = arg.slice(2).split('=', 2);
28
+ options.set(key, value);
29
+ }
30
+ else if (arg.startsWith('--')) {
31
+ const key = arg.slice(2);
32
+ const next = args[i + 1];
33
+ if (next && !next.startsWith('-')) {
34
+ if (/^\d+$/.test(next)) {
35
+ options.set(key, next);
36
+ i++;
37
+ }
38
+ else {
39
+ flags.add(key);
40
+ }
41
+ }
42
+ else {
43
+ flags.add(key);
44
+ }
45
+ }
46
+ else if (arg.startsWith('-')) {
47
+ flags.add(arg.slice(1));
48
+ }
49
+ else {
50
+ remainingArgs.push(arg);
51
+ }
52
+ }
53
+ return { command, args: remainingArgs, flags, options };
54
+ }
55
+ function printHelp() {
56
+ console.log(`
57
+ Teams MCP CLI
58
+
59
+ Commands:
60
+ status Check session and authentication status
61
+ search <query> Search Teams for messages (requires valid token)
62
+ send <message> Send a message to yourself (notes)
63
+ send --to <id> Send a message to a specific conversation
64
+ me Get current user profile (email, name, Teams ID)
65
+ login Log in to Teams (opens browser)
66
+ login --force Force new login (clears existing session)
67
+ help Show this help message
68
+
69
+ Options:
70
+ --json Output results as JSON
71
+
72
+ Pagination Options (for search):
73
+ --from <n> Starting offset (default: 0, for page 2 use --from 25)
74
+ --size <n> Page size (default: 25)
75
+ --maxResults <n> Maximum results to return (default: 25)
76
+
77
+ Send Options:
78
+ --to <conversationId> Send to a specific conversation (default: 48:notes = self)
79
+
80
+ Examples:
81
+ npm run cli -- status
82
+ npm run cli -- search "meeting notes"
83
+ npm run cli -- search "project update" --json
84
+ npm run cli -- search "query" --from 25
85
+ npm run cli -- send "Test message to myself"
86
+ npm run cli -- login --force
87
+ `);
88
+ }
89
+ async function commandStatus(flags) {
90
+ const hasSession = hasSessionState();
91
+ const sessionAge = getSessionAge();
92
+ const tokenStatus = getSubstrateTokenStatus();
93
+ const status = {
94
+ directApi: {
95
+ available: tokenStatus.hasToken,
96
+ expiresAt: tokenStatus.expiresAt,
97
+ minutesRemaining: tokenStatus.minutesRemaining,
98
+ },
99
+ session: {
100
+ exists: hasSession,
101
+ ageHours: sessionAge !== null ? Math.round(sessionAge * 10) / 10 : null,
102
+ likelyExpired: sessionAge !== null ? sessionAge > 12 : null,
103
+ },
104
+ };
105
+ if (flags.has('json')) {
106
+ console.log(JSON.stringify(status, null, 2));
107
+ }
108
+ else {
109
+ console.log('\n📊 Status\n');
110
+ if (status.directApi.available) {
111
+ console.log(`Direct API: ✅ Available (${status.directApi.minutesRemaining} min remaining)`);
112
+ }
113
+ else {
114
+ console.log('Direct API: ❌ No valid token (browser login required)');
115
+ }
116
+ console.log(`Session exists: ${status.session.exists ? '✅ Yes' : '❌ No'}`);
117
+ if (status.session.ageHours !== null) {
118
+ console.log(`Session age: ${status.session.ageHours} hours`);
119
+ if (status.session.likelyExpired) {
120
+ console.log('⚠️ Session may be expired');
121
+ }
122
+ }
123
+ }
124
+ }
125
+ async function commandSearch(query, flags, options) {
126
+ if (!query) {
127
+ console.error('❌ Error: Search query required');
128
+ console.error(' Usage: npm run cli -- search "your query"');
129
+ process.exit(1);
130
+ }
131
+ const asJson = flags.has('json');
132
+ const from = options.has('from') ? parseInt(options.get('from'), 10) : 0;
133
+ const size = options.has('size') ? parseInt(options.get('size'), 10) : 25;
134
+ const maxResults = options.has('maxResults') ? parseInt(options.get('maxResults'), 10) : 25;
135
+ if (!hasValidSubstrateToken()) {
136
+ if (asJson) {
137
+ console.log(JSON.stringify({ success: false, error: 'No valid token. Please run: npm run cli -- login' }, null, 2));
138
+ }
139
+ else {
140
+ console.error('❌ No valid token. Please run: npm run cli -- login');
141
+ }
142
+ process.exit(1);
143
+ }
144
+ if (!asJson) {
145
+ console.log(`\n🔍 Searching for: "${query}"`);
146
+ if (from > 0) {
147
+ console.log(` Offset: ${from}, Size: ${size}`);
148
+ }
149
+ }
150
+ const result = await searchMessages(query, { from, size, maxResults });
151
+ if (!result.ok) {
152
+ if (asJson) {
153
+ console.log(JSON.stringify({ success: false, error: result.error.message }, null, 2));
154
+ }
155
+ else {
156
+ console.error(`❌ Search failed: ${result.error.message}`);
157
+ }
158
+ process.exit(1);
159
+ }
160
+ if (asJson) {
161
+ console.log(JSON.stringify({
162
+ query,
163
+ count: result.value.results.length,
164
+ pagination: {
165
+ from: result.value.pagination.from,
166
+ size: result.value.pagination.size,
167
+ returned: result.value.pagination.returned,
168
+ total: result.value.pagination.total,
169
+ hasMore: result.value.pagination.hasMore,
170
+ nextFrom: result.value.pagination.hasMore
171
+ ? result.value.pagination.from + result.value.pagination.returned
172
+ : undefined,
173
+ },
174
+ results: result.value.results,
175
+ }, null, 2));
176
+ }
177
+ else {
178
+ printResults(result.value.results, result.value.pagination);
179
+ }
180
+ }
181
+ function printResults(results, pagination) {
182
+ console.log(`\n📋 Found ${results.length} results`);
183
+ if (pagination.total !== undefined) {
184
+ console.log(` Total available: ${pagination.total}`);
185
+ }
186
+ if (pagination.hasMore) {
187
+ console.log(` More results available (use --from ${pagination.from + pagination.returned})`);
188
+ }
189
+ console.log();
190
+ for (let i = 0; i < results.length; i++) {
191
+ const r = results[i];
192
+ console.log(`${pagination.from + i + 1}. ${r.content.substring(0, 100).replace(/\n/g, ' ')}${r.content.length > 100 ? '...' : ''}`);
193
+ if (r.sender)
194
+ console.log(` From: ${r.sender}`);
195
+ if (r.timestamp)
196
+ console.log(` Time: ${r.timestamp}`);
197
+ console.log();
198
+ }
199
+ }
200
+ async function commandSend(message, flags, options) {
201
+ if (!message) {
202
+ console.error('❌ Error: Message content required');
203
+ console.error(' Usage: npm run cli -- send "your message"');
204
+ process.exit(1);
205
+ }
206
+ const asJson = flags.has('json');
207
+ const conversationId = options.get('to') || '48:notes';
208
+ const auth = extractMessageAuth();
209
+ if (!auth) {
210
+ console.error('❌ No valid authentication. Please run: npm run cli -- login');
211
+ process.exit(1);
212
+ }
213
+ if (!asJson) {
214
+ if (conversationId === '48:notes') {
215
+ console.log(`\n📝 Sending note to yourself...`);
216
+ }
217
+ else {
218
+ console.log(`\n📤 Sending message to: ${conversationId}`);
219
+ }
220
+ console.log(` Content: "${message.substring(0, 50)}${message.length > 50 ? '...' : ''}"`);
221
+ }
222
+ const result = conversationId === '48:notes'
223
+ ? await sendNoteToSelf(message)
224
+ : await sendMessage(conversationId, message);
225
+ if (asJson) {
226
+ console.log(JSON.stringify(result.ok
227
+ ? { success: true, ...result.value }
228
+ : { success: false, error: result.error.message }, null, 2));
229
+ }
230
+ else {
231
+ if (result.ok) {
232
+ console.log('\n✅ Message sent successfully!');
233
+ console.log(` Message ID: ${result.value.messageId}`);
234
+ if (result.value.timestamp) {
235
+ console.log(` Timestamp: ${new Date(result.value.timestamp).toISOString()}`);
236
+ }
237
+ }
238
+ else {
239
+ console.error(`\n❌ Failed to send message: ${result.error.message}`);
240
+ process.exit(1);
241
+ }
242
+ }
243
+ }
244
+ async function commandMe(flags) {
245
+ const asJson = flags.has('json');
246
+ const profile = getUserProfile();
247
+ if (!profile) {
248
+ if (asJson) {
249
+ console.log(JSON.stringify({ success: false, error: 'No valid session' }, null, 2));
250
+ }
251
+ else {
252
+ console.error('❌ No valid session. Please run: npm run cli -- login');
253
+ }
254
+ process.exit(1);
255
+ }
256
+ if (asJson) {
257
+ console.log(JSON.stringify({ success: true, profile }, null, 2));
258
+ }
259
+ else {
260
+ console.log('\n👤 Current User\n');
261
+ console.log(` Name: ${profile.displayName}`);
262
+ console.log(` Email: ${profile.email}`);
263
+ console.log(` ID: ${profile.id}`);
264
+ console.log(` MRI: ${profile.mri}`);
265
+ if (profile.tenantId) {
266
+ console.log(` Tenant: ${profile.tenantId}`);
267
+ }
268
+ }
269
+ }
270
+ async function commandLogin(flags) {
271
+ const force = flags.has('force');
272
+ if (force) {
273
+ console.log('🔄 Forcing new login (clearing existing session)...');
274
+ clearSessionState();
275
+ clearTokenCache();
276
+ }
277
+ else {
278
+ console.log('🔐 Starting login flow...');
279
+ }
280
+ let manager = null;
281
+ try {
282
+ manager = await createBrowserContext({ headless: false });
283
+ if (force) {
284
+ await forceNewLogin(manager.page, manager.context, (msg) => console.log(` ${msg}`));
285
+ }
286
+ else {
287
+ await ensureAuthenticated(manager.page, manager.context, (msg) => console.log(` ${msg}`));
288
+ }
289
+ console.log('\n✅ Login successful! Session has been saved.');
290
+ console.log(' You can now run searches in headless mode.');
291
+ }
292
+ finally {
293
+ if (manager) {
294
+ await closeBrowser(manager, true);
295
+ }
296
+ }
297
+ }
298
+ async function main() {
299
+ const { command, args, flags, options } = parseArgs();
300
+ try {
301
+ switch (command) {
302
+ case 'status':
303
+ await commandStatus(flags);
304
+ break;
305
+ case 'search':
306
+ await commandSearch(args.join(' '), flags, options);
307
+ break;
308
+ case 'send':
309
+ await commandSend(args.join(' '), flags, options);
310
+ break;
311
+ case 'me':
312
+ await commandMe(flags);
313
+ break;
314
+ case 'login':
315
+ await commandLogin(flags);
316
+ break;
317
+ case 'help':
318
+ default:
319
+ printHelp();
320
+ break;
321
+ }
322
+ }
323
+ catch (error) {
324
+ console.error('\n❌ Error:', error instanceof Error ? error.message : String(error));
325
+ process.exit(1);
326
+ }
327
+ }
328
+ main();
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env npx tsx
2
+ /**
3
+ * Debug script to inspect the Teams search page.
4
+ * Takes screenshots and dumps page structure for debugging selectors.
5
+ *
6
+ * Usage:
7
+ * npm run debug:search
8
+ * npm run debug:search -- "search query"
9
+ */
10
+ export {};
@@ -0,0 +1,147 @@
1
+ #!/usr/bin/env npx tsx
2
+ /**
3
+ * Debug script to inspect the Teams search page.
4
+ * Takes screenshots and dumps page structure for debugging selectors.
5
+ *
6
+ * Usage:
7
+ * npm run debug:search
8
+ * npm run debug:search -- "search query"
9
+ */
10
+ import { createBrowserContext, closeBrowser } from '../browser/context.js';
11
+ import { ensureAuthenticated } from '../browser/auth.js';
12
+ import * as fs from 'fs';
13
+ import * as path from 'path';
14
+ import { fileURLToPath } from 'url';
15
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
16
+ const PROJECT_ROOT = path.resolve(__dirname, '../..');
17
+ const DEBUG_DIR = path.join(PROJECT_ROOT, 'debug-output');
18
+ async function main() {
19
+ const query = process.argv[2] ?? 'test';
20
+ console.log('🔍 Debug Search Script\n');
21
+ console.log(`Query: "${query}"`);
22
+ // Ensure debug output directory exists
23
+ if (!fs.existsSync(DEBUG_DIR)) {
24
+ fs.mkdirSync(DEBUG_DIR, { recursive: true });
25
+ }
26
+ const manager = await createBrowserContext({ headless: false });
27
+ try {
28
+ // Authenticate
29
+ console.log('\n1. Authenticating...');
30
+ await ensureAuthenticated(manager.page, manager.context);
31
+ // Take initial screenshot
32
+ console.log('\n2. Taking initial screenshot...');
33
+ await manager.page.screenshot({
34
+ path: path.join(DEBUG_DIR, '01-initial.png'),
35
+ fullPage: true
36
+ });
37
+ // Wait for page to stabilise
38
+ await manager.page.waitForTimeout(3000);
39
+ // Open search with keyboard shortcut
40
+ console.log('\n3. Opening search (Cmd+E)...');
41
+ const isMac = process.platform === 'darwin';
42
+ await manager.page.keyboard.press(isMac ? 'Meta+e' : 'Control+e');
43
+ await manager.page.waitForTimeout(2000);
44
+ await manager.page.screenshot({
45
+ path: path.join(DEBUG_DIR, '02-search-opened.png'),
46
+ fullPage: true
47
+ });
48
+ // Find and list all inputs
49
+ console.log('\n4. Scanning for input elements...');
50
+ const inputs = await manager.page.locator('input').all();
51
+ console.log(` Found ${inputs.length} input elements:`);
52
+ for (let i = 0; i < inputs.length; i++) {
53
+ const input = inputs[i];
54
+ try {
55
+ const isVisible = await input.isVisible();
56
+ if (!isVisible)
57
+ continue;
58
+ const placeholder = await input.getAttribute('placeholder') ?? '';
59
+ const ariaLabel = await input.getAttribute('aria-label') ?? '';
60
+ const dataTid = await input.getAttribute('data-tid') ?? '';
61
+ const type = await input.getAttribute('type') ?? '';
62
+ const id = await input.getAttribute('id') ?? '';
63
+ console.log(` [${i}] visible=true, placeholder="${placeholder}", aria-label="${ariaLabel}", data-tid="${dataTid}", type="${type}", id="${id}"`);
64
+ }
65
+ catch {
66
+ // Skip
67
+ }
68
+ }
69
+ // Try to find and use search input
70
+ console.log('\n5. Looking for search input...');
71
+ const searchSelectors = [
72
+ 'input[placeholder*="Search" i]',
73
+ 'input[aria-label*="Search" i]',
74
+ '[data-tid*="search"] input',
75
+ 'input[type="search"]',
76
+ ];
77
+ let searchInput = null;
78
+ for (const sel of searchSelectors) {
79
+ const loc = manager.page.locator(sel).first();
80
+ if (await loc.count() > 0 && await loc.isVisible()) {
81
+ console.log(` Found with selector: ${sel}`);
82
+ searchInput = loc;
83
+ break;
84
+ }
85
+ }
86
+ if (searchInput) {
87
+ // Type the query
88
+ console.log('\n6. Typing query...');
89
+ await searchInput.fill(query);
90
+ await manager.page.waitForTimeout(500);
91
+ await manager.page.screenshot({
92
+ path: path.join(DEBUG_DIR, '03-query-typed.png'),
93
+ fullPage: true
94
+ });
95
+ // Submit
96
+ console.log('\n7. Submitting search (Enter)...');
97
+ await manager.page.keyboard.press('Enter');
98
+ await manager.page.waitForTimeout(5000);
99
+ await manager.page.screenshot({
100
+ path: path.join(DEBUG_DIR, '04-results.png'),
101
+ fullPage: true
102
+ });
103
+ // Scan for result elements
104
+ console.log('\n8. Scanning for result elements...');
105
+ const resultSelectors = [
106
+ '[data-tid*="search"]',
107
+ '[data-tid*="result"]',
108
+ '[role="listitem"]',
109
+ '[role="option"]',
110
+ '.search-result',
111
+ '[data-tid*="message"]',
112
+ ];
113
+ for (const sel of resultSelectors) {
114
+ const count = await manager.page.locator(sel).count();
115
+ if (count > 0) {
116
+ console.log(` ${sel}: ${count} elements`);
117
+ // Get first element's text content preview
118
+ const first = manager.page.locator(sel).first();
119
+ const text = await first.textContent().catch(() => null);
120
+ if (text) {
121
+ console.log(` Preview: "${text.substring(0, 80).replace(/\n/g, ' ')}..."`);
122
+ }
123
+ }
124
+ }
125
+ // Dump page HTML structure (simplified)
126
+ console.log('\n9. Dumping main content structure...');
127
+ const mainContent = await manager.page.locator('main, [role="main"], #app, .app-container').first();
128
+ if (await mainContent.count() > 0) {
129
+ const html = await mainContent.innerHTML();
130
+ fs.writeFileSync(path.join(DEBUG_DIR, 'page-structure.html'), html.substring(0, 50000) // First 50KB
131
+ );
132
+ console.log(' Saved to debug-output/page-structure.html');
133
+ }
134
+ }
135
+ else {
136
+ console.log(' ❌ Could not find search input');
137
+ }
138
+ console.log('\n10. Keeping browser open for 30 seconds for manual inspection...');
139
+ console.log(` Screenshots saved to: ${DEBUG_DIR}`);
140
+ await manager.page.waitForTimeout(30000);
141
+ }
142
+ finally {
143
+ await closeBrowser(manager, true);
144
+ }
145
+ console.log('\n✅ Debug session complete');
146
+ }
147
+ main().catch(console.error);
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env npx tsx
2
+ /**
3
+ * Manual testing script for Teams MCP functionality.
4
+ * Runs through the core features to verify they work.
5
+ *
6
+ * Usage:
7
+ * npm run test:manual
8
+ * npm run test:manual -- --search "your query"
9
+ * npm run test:manual -- --headless
10
+ */
11
+ export {};
@@ -0,0 +1,160 @@
1
+ #!/usr/bin/env npx tsx
2
+ /**
3
+ * Manual testing script for Teams MCP functionality.
4
+ * Runs through the core features to verify they work.
5
+ *
6
+ * Usage:
7
+ * npm run test:manual
8
+ * npm run test:manual -- --search "your query"
9
+ * npm run test:manual -- --headless
10
+ */
11
+ import { createBrowserContext, closeBrowser } from '../browser/context.js';
12
+ import { getAuthStatus, ensureAuthenticated } from '../browser/auth.js';
13
+ import { searchTeams } from '../teams/search.js';
14
+ import { hasSessionState, getSessionAge } from '../auth/session-store.js';
15
+ function parseArgs() {
16
+ const args = process.argv.slice(2);
17
+ const options = {
18
+ headless: false,
19
+ };
20
+ for (let i = 0; i < args.length; i++) {
21
+ if (args[i] === '--headless') {
22
+ options.headless = true;
23
+ }
24
+ else if (args[i] === '--search' && args[i + 1]) {
25
+ options.searchQuery = args[i + 1];
26
+ i++;
27
+ }
28
+ }
29
+ return options;
30
+ }
31
+ function log(message, indent = 0) {
32
+ const prefix = ' '.repeat(indent);
33
+ console.log(`${prefix}${message}`);
34
+ }
35
+ function logSection(title) {
36
+ console.log('\n' + '─'.repeat(50));
37
+ console.log(` ${title}`);
38
+ console.log('─'.repeat(50));
39
+ }
40
+ async function testSessionState() {
41
+ logSection('Session State');
42
+ const hasSession = hasSessionState();
43
+ const sessionAge = getSessionAge();
44
+ log(`Session exists: ${hasSession ? '✅ Yes' : '❌ No'}`);
45
+ if (sessionAge !== null) {
46
+ const ageHours = sessionAge.toFixed(1);
47
+ const isOld = sessionAge > 12;
48
+ log(`Session age: ${ageHours} hours ${isOld ? '⚠️ (may be expired)' : '✅'}`);
49
+ }
50
+ return hasSession;
51
+ }
52
+ async function testBrowserContext(headless) {
53
+ logSection('Browser Context');
54
+ log(`Creating browser (headless: ${headless})...`);
55
+ try {
56
+ const manager = await createBrowserContext({ headless });
57
+ log(`Browser launched: ✅`);
58
+ log(`New session: ${manager.isNewSession ? 'Yes (will need login)' : 'No (restored from saved)'}`);
59
+ return manager;
60
+ }
61
+ catch (error) {
62
+ log(`Browser launch failed: ❌`);
63
+ log(`Error: ${error instanceof Error ? error.message : String(error)}`, 1);
64
+ return null;
65
+ }
66
+ }
67
+ async function testAuthentication(manager) {
68
+ logSection('Authentication');
69
+ log('Checking authentication status...');
70
+ try {
71
+ await ensureAuthenticated(manager.page, manager.context, (msg) => log(` ${msg}`));
72
+ const status = await getAuthStatus(manager.page);
73
+ log(`Authenticated: ${status.isAuthenticated ? '✅ Yes' : '❌ No'}`);
74
+ log(`Current URL: ${status.currentUrl}`, 1);
75
+ return status.isAuthenticated;
76
+ }
77
+ catch (error) {
78
+ log(`Authentication failed: ❌`);
79
+ log(`Error: ${error instanceof Error ? error.message : String(error)}`, 1);
80
+ return false;
81
+ }
82
+ }
83
+ async function testSearch(manager, query) {
84
+ logSection('Search');
85
+ log(`Searching for: "${query}"...`);
86
+ try {
87
+ const results = await searchTeams(manager.page, query, {
88
+ maxResults: 10,
89
+ waitMs: 8000,
90
+ });
91
+ log(`Results found: ${results.length}`);
92
+ if (results.length > 0) {
93
+ log('Sample results:', 1);
94
+ for (const result of results.slice(0, 3)) {
95
+ const preview = result.content.substring(0, 80).replace(/\n/g, ' ');
96
+ log(`• ${preview}${result.content.length > 80 ? '...' : ''}`, 2);
97
+ if (result.sender) {
98
+ log(` From: ${result.sender}`, 2);
99
+ }
100
+ }
101
+ }
102
+ return results.length > 0;
103
+ }
104
+ catch (error) {
105
+ log(`Search failed: ❌`);
106
+ log(`Error: ${error instanceof Error ? error.message : String(error)}`, 1);
107
+ return false;
108
+ }
109
+ }
110
+ async function runTests() {
111
+ console.log('\n🧪 Teams MCP Manual Test');
112
+ console.log('========================\n');
113
+ const options = parseArgs();
114
+ if (options.headless) {
115
+ log('Running in headless mode');
116
+ }
117
+ else {
118
+ log('Running with visible browser (use --headless to run headless)');
119
+ }
120
+ // Test 1: Session state
121
+ const hasSession = await testSessionState();
122
+ if (!hasSession && options.headless) {
123
+ log('\n⚠️ No session found. Cannot run headless without a saved session.');
124
+ log(' Run without --headless first to log in, or run: npm run research');
125
+ process.exit(1);
126
+ }
127
+ // Test 2: Browser context
128
+ const manager = await testBrowserContext(options.headless);
129
+ if (!manager) {
130
+ process.exit(1);
131
+ }
132
+ try {
133
+ // Test 3: Authentication
134
+ const isAuthenticated = await testAuthentication(manager);
135
+ if (!isAuthenticated) {
136
+ log('\n⚠️ Not authenticated. Please log in manually in the browser window.');
137
+ log(' Waiting for authentication...');
138
+ // The ensureAuthenticated call above should have handled this
139
+ }
140
+ // Test 4: Search (if query provided or use default)
141
+ const searchQuery = options.searchQuery ?? 'test';
142
+ await testSearch(manager, searchQuery);
143
+ // Summary
144
+ logSection('Summary');
145
+ log('Tests completed. Review results above.');
146
+ if (!options.headless) {
147
+ log('\nBrowser will remain open for 10 seconds for inspection...');
148
+ await manager.page.waitForTimeout(10000);
149
+ }
150
+ }
151
+ finally {
152
+ log('\nClosing browser...');
153
+ await closeBrowser(manager, true);
154
+ log('Done.');
155
+ }
156
+ }
157
+ runTests().catch((error) => {
158
+ console.error('\n❌ Test failed with error:', error);
159
+ process.exit(1);
160
+ });
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env npx tsx
2
+ /**
3
+ * MCP Protocol Test Harness
4
+ *
5
+ * Tests the MCP server by connecting a client through the actual MCP protocol,
6
+ * rather than calling underlying functions directly. This ensures the full
7
+ * protocol layer works correctly.
8
+ *
9
+ * Usage:
10
+ * npm run test:mcp # List tools and check status
11
+ * npm run test:mcp -- search "query" # Search for messages (shortcut)
12
+ * npm run test:mcp -- teams_search --query "q" # Generic tool call
13
+ * npm run test:mcp -- --json # Output as JSON
14
+ *
15
+ * Any unrecognised command is treated as a tool name. Use --key value for parameters.
16
+ */
17
+ export {};