msteams-mcp 0.30.0 → 0.30.2

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.
@@ -0,0 +1,317 @@
1
+ /**
2
+ * Focused research script for Teams search API.
3
+ *
4
+ * Captures full request/response bodies for Substrate search endpoints
5
+ * to understand pagination, query format, and response structure.
6
+ *
7
+ * Usage:
8
+ * npm run research:search
9
+ *
10
+ * Instructions:
11
+ * 1. Run this script
12
+ * 2. Perform a search in Teams
13
+ * 3. Click "Messages" tab or "See more results"
14
+ * 4. Scroll through results to trigger pagination
15
+ * 5. Press Ctrl+C to stop and save findings
16
+ */
17
+ import { chromium } from 'playwright';
18
+ import * as fs from 'fs';
19
+ import * as path from 'path';
20
+ import { fileURLToPath } from 'url';
21
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
22
+ const PROJECT_ROOT = path.resolve(__dirname, '../..');
23
+ const SESSION_STATE_PATH = path.join(PROJECT_ROOT, 'session-state.json');
24
+ const SEARCH_FINDINGS_PATH = path.join(PROJECT_ROOT, 'search-api-findings.json');
25
+ const captures = [];
26
+ // Focus specifically on search-related endpoints
27
+ function isSearchEndpoint(url) {
28
+ const searchPatterns = [
29
+ /substrate\.office\.com\/search/i,
30
+ /\/api\/v\d+\/query/i,
31
+ /\/api\/v\d+\/suggestions/i,
32
+ /\/search\//i,
33
+ ];
34
+ return searchPatterns.some(p => p.test(url));
35
+ }
36
+ async function captureSearchRequest(request) {
37
+ const url = request.url();
38
+ if (!isSearchEndpoint(url))
39
+ return;
40
+ const postData = request.postData();
41
+ let body;
42
+ if (postData) {
43
+ try {
44
+ body = JSON.parse(postData);
45
+ }
46
+ catch {
47
+ body = postData;
48
+ }
49
+ }
50
+ const capture = {
51
+ timestamp: new Date().toISOString(),
52
+ type: 'request',
53
+ url,
54
+ method: request.method(),
55
+ body,
56
+ headers: request.headers(),
57
+ };
58
+ captures.push(capture);
59
+ console.log('\n' + '━'.repeat(80));
60
+ console.log(`šŸ“¤ SEARCH REQUEST: ${request.method()} ${url}`);
61
+ console.log('━'.repeat(80));
62
+ if (body) {
63
+ console.log('\nšŸ“‹ Request Body:');
64
+ console.log(JSON.stringify(body, null, 2));
65
+ }
66
+ // Highlight pagination-related fields
67
+ if (body && typeof body === 'object') {
68
+ const bodyObj = body;
69
+ const paginationFields = ['Size', 'From', 'Skip', 'Top', 'Offset', 'PageSize', 'Page', 'MaxResults'];
70
+ const foundPagination = {};
71
+ function findPaginationFields(obj, prefix = '') {
72
+ if (!obj || typeof obj !== 'object')
73
+ return;
74
+ for (const [key, value] of Object.entries(obj)) {
75
+ const fullKey = prefix ? `${prefix}.${key}` : key;
76
+ if (paginationFields.some(p => key.toLowerCase().includes(p.toLowerCase()))) {
77
+ foundPagination[fullKey] = value;
78
+ }
79
+ if (typeof value === 'object') {
80
+ findPaginationFields(value, fullKey);
81
+ }
82
+ }
83
+ }
84
+ findPaginationFields(bodyObj);
85
+ if (Object.keys(foundPagination).length > 0) {
86
+ console.log('\nšŸ“Š Pagination Fields Found:');
87
+ console.log(JSON.stringify(foundPagination, null, 2));
88
+ }
89
+ }
90
+ }
91
+ async function captureSearchResponse(response) {
92
+ const url = response.url();
93
+ if (!isSearchEndpoint(url))
94
+ return;
95
+ let body;
96
+ try {
97
+ const contentType = response.headers()['content-type'] || '';
98
+ if (contentType.includes('application/json')) {
99
+ const text = await response.text();
100
+ body = JSON.parse(text);
101
+ }
102
+ }
103
+ catch {
104
+ // Response body may not be available
105
+ }
106
+ const capture = {
107
+ timestamp: new Date().toISOString(),
108
+ type: 'response',
109
+ url,
110
+ status: response.status(),
111
+ body,
112
+ };
113
+ captures.push(capture);
114
+ console.log('\n' + '━'.repeat(80));
115
+ console.log(`šŸ“„ SEARCH RESPONSE: ${response.status()} ${url}`);
116
+ console.log('━'.repeat(80));
117
+ if (body && typeof body === 'object') {
118
+ const bodyObj = body;
119
+ // Show structure summary
120
+ console.log('\nšŸ“‹ Response Structure:');
121
+ const summarise = (obj, depth = 0) => {
122
+ if (depth > 2)
123
+ return '...';
124
+ if (Array.isArray(obj)) {
125
+ return `Array[${obj.length}]${obj.length > 0 ? ` of ${summarise(obj[0], depth + 1)}` : ''}`;
126
+ }
127
+ if (obj && typeof obj === 'object') {
128
+ const keys = Object.keys(obj);
129
+ if (keys.length > 8) {
130
+ return `{${keys.slice(0, 5).join(', ')}, ... +${keys.length - 5} more}`;
131
+ }
132
+ return `{${keys.join(', ')}}`;
133
+ }
134
+ return typeof obj;
135
+ };
136
+ console.log(summarise(bodyObj));
137
+ // Show pagination-related response fields
138
+ const paginationResponseFields = ['Total', 'TotalCount', 'Count', 'HasMore', 'HasNext', 'NextLink', 'SkipToken', 'ContinuationToken'];
139
+ const foundPagination = {};
140
+ function findPaginationFields(obj, prefix = '') {
141
+ if (!obj || typeof obj !== 'object')
142
+ return;
143
+ for (const [key, value] of Object.entries(obj)) {
144
+ const fullKey = prefix ? `${prefix}.${key}` : key;
145
+ if (paginationResponseFields.some(p => key.toLowerCase().includes(p.toLowerCase()))) {
146
+ foundPagination[fullKey] = value;
147
+ }
148
+ if (typeof value === 'object' && !Array.isArray(value)) {
149
+ findPaginationFields(value, fullKey);
150
+ }
151
+ }
152
+ }
153
+ findPaginationFields(bodyObj);
154
+ if (Object.keys(foundPagination).length > 0) {
155
+ console.log('\nšŸ“Š Pagination Response Fields:');
156
+ console.log(JSON.stringify(foundPagination, null, 2));
157
+ }
158
+ // Show results count
159
+ const countArrays = (obj) => {
160
+ const results = [];
161
+ function traverse(o, path) {
162
+ if (Array.isArray(o) && o.length > 0) {
163
+ results.push({ path: path || 'root', count: o.length });
164
+ }
165
+ if (o && typeof o === 'object' && !Array.isArray(o)) {
166
+ for (const [key, value] of Object.entries(o)) {
167
+ traverse(value, path ? `${path}.${key}` : key);
168
+ }
169
+ }
170
+ }
171
+ traverse(obj, '');
172
+ return results;
173
+ };
174
+ const arrays = countArrays(bodyObj);
175
+ if (arrays.length > 0) {
176
+ console.log('\nšŸ“ˆ Result Counts:');
177
+ arrays.forEach(a => console.log(` ${a.path}: ${a.count} items`));
178
+ }
179
+ // Show first result sample if available
180
+ const findFirstResult = (obj) => {
181
+ if (Array.isArray(obj) && obj.length > 0) {
182
+ const first = obj[0];
183
+ if (first && typeof first === 'object' && Object.keys(first).length > 2) {
184
+ return first;
185
+ }
186
+ }
187
+ if (obj && typeof obj === 'object') {
188
+ for (const value of Object.values(obj)) {
189
+ const found = findFirstResult(value);
190
+ if (found)
191
+ return found;
192
+ }
193
+ }
194
+ return null;
195
+ };
196
+ const firstResult = findFirstResult(bodyObj);
197
+ if (firstResult) {
198
+ console.log('\nšŸ“„ First Result Sample:');
199
+ console.log(JSON.stringify(firstResult, null, 2).substring(0, 1000));
200
+ }
201
+ }
202
+ }
203
+ async function checkAuthentication(page) {
204
+ const url = page.url();
205
+ if (url.includes('login.microsoftonline.com') ||
206
+ url.includes('login.live.com') ||
207
+ url.includes('login.microsoft.com')) {
208
+ return false;
209
+ }
210
+ if (url.includes('teams.microsoft.com')) {
211
+ try {
212
+ await page.waitForTimeout(2000);
213
+ const hasAppBar = await page.locator('[data-tid="app-bar"]').count() > 0;
214
+ const hasSearchBox = await page.locator('[data-tid="search-box"]').count() > 0 ||
215
+ await page.locator('input[placeholder*="Search"]').count() > 0;
216
+ return hasAppBar || hasSearchBox;
217
+ }
218
+ catch {
219
+ return false;
220
+ }
221
+ }
222
+ return false;
223
+ }
224
+ async function waitForAuthentication(page) {
225
+ console.log('\nšŸ” Please log in to Microsoft Teams...');
226
+ while (true) {
227
+ if (await checkAuthentication(page)) {
228
+ console.log('āœ… Authentication detected!');
229
+ break;
230
+ }
231
+ await page.waitForTimeout(2000);
232
+ }
233
+ }
234
+ async function saveFindings() {
235
+ const findings = {
236
+ capturedAt: new Date().toISOString(),
237
+ totalCaptures: captures.length,
238
+ requests: captures.filter(c => c.type === 'request'),
239
+ responses: captures.filter(c => c.type === 'response'),
240
+ };
241
+ fs.writeFileSync(SEARCH_FINDINGS_PATH, JSON.stringify(findings, null, 2));
242
+ console.log(`\nšŸ’¾ Search findings saved to ${SEARCH_FINDINGS_PATH}`);
243
+ }
244
+ async function main() {
245
+ console.log('šŸ” Teams Search API Research');
246
+ console.log('============================\n');
247
+ let browser;
248
+ let context;
249
+ try {
250
+ browser = await chromium.launch({ headless: false });
251
+ const hasSessionState = fs.existsSync(SESSION_STATE_PATH);
252
+ if (hasSessionState) {
253
+ console.log('šŸ“‚ Restoring session state...');
254
+ context = await browser.newContext({
255
+ storageState: SESSION_STATE_PATH,
256
+ viewport: { width: 1400, height: 900 },
257
+ });
258
+ }
259
+ else {
260
+ console.log('šŸ“‚ No session state found, starting fresh...');
261
+ context = await browser.newContext({
262
+ viewport: { width: 1400, height: 900 },
263
+ });
264
+ }
265
+ const page = await context.newPage();
266
+ // Set up network interception for search endpoints only
267
+ page.on('request', captureSearchRequest);
268
+ page.on('response', captureSearchResponse);
269
+ console.log('🌐 Navigating to Teams...');
270
+ await page.goto('https://teams.microsoft.com', { waitUntil: 'domcontentloaded' });
271
+ const isAuthenticated = await checkAuthentication(page);
272
+ if (!isAuthenticated) {
273
+ await waitForAuthentication(page);
274
+ await context.storageState({ path: SESSION_STATE_PATH });
275
+ console.log('āœ… Session state saved!');
276
+ }
277
+ else {
278
+ console.log('āœ… Already authenticated!');
279
+ }
280
+ console.log('\n' + '═'.repeat(60));
281
+ console.log('šŸ”¬ SEARCH RESEARCH MODE');
282
+ console.log('═'.repeat(60));
283
+ console.log('\nTo capture search API data:');
284
+ console.log('');
285
+ console.log(' 1. Press Cmd/Ctrl+E to open search');
286
+ console.log(' 2. Type a search query and press Enter');
287
+ console.log(' 3. Click "Messages" tab to see message results');
288
+ console.log(' 4. Click "See all results" if available');
289
+ console.log(' 5. Scroll through results to trigger pagination');
290
+ console.log('');
291
+ console.log('Watch for šŸ“Š Pagination Fields in the output!');
292
+ console.log('');
293
+ console.log('Press Ctrl+C when done to save findings.');
294
+ console.log('═'.repeat(60) + '\n');
295
+ await new Promise((resolve) => {
296
+ process.on('SIGINT', () => {
297
+ console.log('\n\nā¹ļø Stopping research...');
298
+ resolve();
299
+ });
300
+ });
301
+ await context.storageState({ path: SESSION_STATE_PATH });
302
+ await saveFindings();
303
+ }
304
+ catch (error) {
305
+ console.error('āŒ Error:', error);
306
+ throw error;
307
+ }
308
+ finally {
309
+ if (context)
310
+ await context.close();
311
+ if (browser)
312
+ await browser.close();
313
+ }
314
+ console.log('\nāœ… Search research complete!');
315
+ console.log(` Review findings in: ${SEARCH_FINDINGS_PATH}`);
316
+ }
317
+ main().catch(console.error);
@@ -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,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 {};