msteams-mcp 0.30.2 → 0.30.3

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.
@@ -1,287 +0,0 @@
1
- /**
2
- * Research script to explore Teams web app behaviour.
3
- *
4
- * This script:
5
- * 1. Launches a browser with persistent context
6
- * 2. Navigates to Teams and handles authentication
7
- * 3. Monitors network requests to discover API endpoints
8
- * 4. Allows manual interaction to trigger searches
9
- * 5. Logs discovered endpoints and data structures
10
- */
11
- import { chromium } from 'playwright';
12
- import * as fs from 'fs';
13
- import * as path from 'path';
14
- import { PROJECT_ROOT, USER_DATA_DIR, readSessionState, writeSessionState, } from '../auth/session-store.js';
15
- const FINDINGS_PATH = path.join(PROJECT_ROOT, 'research-findings.json');
16
- // Track interesting API calls
17
- const capturedNetworkData = {
18
- requests: [],
19
- responses: [],
20
- };
21
- // Patterns that indicate interesting API calls
22
- const SEARCH_PATTERNS = [
23
- /search/i,
24
- /query/i,
25
- /find/i,
26
- /substrate/i,
27
- /graph\.microsoft/i,
28
- /teams.*api/i,
29
- /chatservice/i,
30
- /emea\.ng\.msg/i,
31
- /transcript/i,
32
- /recording/i,
33
- /onlineMeeting/i,
34
- /callRecord/i,
35
- /\.vtt/i,
36
- /asyncgw/i,
37
- /mediaservice/i,
38
- ];
39
- function isInterestingUrl(url) {
40
- // Skip static assets, telemetry, and auth-related noise
41
- const skipPatterns = [
42
- /\.(js|css|png|jpg|jpeg|gif|svg|woff|woff2|ttf|ico)(\?|$)/i,
43
- /telemetry/i,
44
- /analytics/i,
45
- /logging/i,
46
- /beacon/i,
47
- /\.clarity\./i,
48
- /fonts\./i,
49
- /static/i,
50
- ];
51
- if (skipPatterns.some(pattern => pattern.test(url))) {
52
- return false;
53
- }
54
- // Include if it matches search patterns or is a potential API call
55
- return SEARCH_PATTERNS.some(pattern => pattern.test(url)) ||
56
- url.includes('/api/') ||
57
- url.includes('/v1/') ||
58
- url.includes('/v2/');
59
- }
60
- async function captureRequest(request) {
61
- const url = request.url();
62
- if (!isInterestingUrl(url)) {
63
- return;
64
- }
65
- const captured = {
66
- url,
67
- method: request.method(),
68
- headers: request.headers(),
69
- postData: request.postData() ?? undefined,
70
- timestamp: new Date().toISOString(),
71
- resourceType: request.resourceType(),
72
- };
73
- capturedNetworkData.requests.push(captured);
74
- console.log(`\nšŸ“¤ REQUEST: ${request.method()} ${url}`);
75
- if (captured.postData) {
76
- try {
77
- const parsed = JSON.parse(captured.postData);
78
- // Show full body for message sends (to see mention format)
79
- const isMessageSend = url.includes('/messages') && request.method() === 'POST';
80
- const limit = isMessageSend ? 10000 : 500;
81
- console.log(' Body:', JSON.stringify(parsed, null, 2).substring(0, limit));
82
- }
83
- catch {
84
- console.log(' Body:', captured.postData.substring(0, 200));
85
- }
86
- }
87
- }
88
- async function captureResponse(response) {
89
- const url = response.url();
90
- if (!isInterestingUrl(url)) {
91
- return;
92
- }
93
- let body;
94
- try {
95
- const contentType = response.headers()['content-type'] || '';
96
- if (contentType.includes('application/json') || contentType.includes('text/vtt') || contentType.includes('text/plain')) {
97
- body = await response.text();
98
- }
99
- }
100
- catch {
101
- // Response body may not be available
102
- }
103
- const captured = {
104
- url,
105
- status: response.status(),
106
- statusText: response.statusText(),
107
- headers: response.headers(),
108
- body,
109
- timestamp: new Date().toISOString(),
110
- };
111
- capturedNetworkData.responses.push(captured);
112
- console.log(`\nšŸ“„ RESPONSE: ${response.status()} ${url}`);
113
- if (body) {
114
- try {
115
- const parsed = JSON.parse(body);
116
- console.log(' Body preview:', JSON.stringify(parsed, null, 2).substring(0, 500));
117
- }
118
- catch {
119
- console.log(' Body preview:', body.substring(0, 200));
120
- }
121
- }
122
- }
123
- async function saveFindings() {
124
- // Deduplicate and summarise findings
125
- const uniqueEndpoints = new Map();
126
- for (const req of capturedNetworkData.requests) {
127
- const key = `${req.method}:${new URL(req.url).pathname}`;
128
- if (!uniqueEndpoints.has(key)) {
129
- uniqueEndpoints.set(key, req);
130
- }
131
- }
132
- const findings = {
133
- discoveredAt: new Date().toISOString(),
134
- uniqueEndpoints: Array.from(uniqueEndpoints.values()).map(req => ({
135
- method: req.method,
136
- url: req.url,
137
- hasPostData: !!req.postData,
138
- postDataSample: req.postData?.substring(0, 500),
139
- })),
140
- allRequests: capturedNetworkData.requests,
141
- allResponses: capturedNetworkData.responses,
142
- };
143
- fs.writeFileSync(FINDINGS_PATH, JSON.stringify(findings, null, 2));
144
- console.log(`\nšŸ’¾ Findings saved to ${FINDINGS_PATH}`);
145
- }
146
- async function checkAuthentication(page) {
147
- const url = page.url();
148
- // Check if we're on a login page
149
- if (url.includes('login.microsoftonline.com') ||
150
- url.includes('login.live.com') ||
151
- url.includes('login.microsoft.com')) {
152
- return false;
153
- }
154
- // Check if we're on the Teams app
155
- if (url.includes('teams.microsoft.com')) {
156
- // Look for indicators that we're logged in
157
- try {
158
- // Wait briefly for the app to load
159
- await page.waitForTimeout(2000);
160
- // Check for common authenticated elements
161
- const hasAppBar = await page.locator('[data-tid="app-bar"]').count() > 0;
162
- const hasSearchBox = await page.locator('[data-tid="search-box"]').count() > 0 ||
163
- await page.locator('input[placeholder*="Search"]').count() > 0;
164
- return hasAppBar || hasSearchBox;
165
- }
166
- catch {
167
- return false;
168
- }
169
- }
170
- return false;
171
- }
172
- async function waitForAuthentication(page) {
173
- console.log('\nšŸ” Please log in to Microsoft Teams in the browser window...');
174
- console.log(' The script will continue once you are authenticated.\n');
175
- // Poll for authentication
176
- while (true) {
177
- if (await checkAuthentication(page)) {
178
- console.log('āœ… Authentication detected!');
179
- break;
180
- }
181
- await page.waitForTimeout(2000);
182
- }
183
- }
184
- async function main() {
185
- console.log('šŸ” Teams Research Script');
186
- console.log('========================\n');
187
- console.log('This script will help discover how Teams web app works.\n');
188
- // Ensure user data directory exists
189
- if (!fs.existsSync(USER_DATA_DIR)) {
190
- fs.mkdirSync(USER_DATA_DIR, { recursive: true });
191
- }
192
- let browser;
193
- let context;
194
- try {
195
- // Launch browser with persistent context
196
- const channel = process.platform === 'win32' ? 'msedge' : 'chrome';
197
- browser = await chromium.launch({
198
- headless: false, // Must be visible for manual login
199
- channel,
200
- });
201
- // Check if we have saved session state (uses encrypted store)
202
- const existingState = readSessionState();
203
- if (existingState) {
204
- console.log('šŸ“‚ Found existing session state, attempting to restore...');
205
- context = await browser.newContext({
206
- storageState: existingState,
207
- viewport: { width: 1280, height: 800 },
208
- });
209
- }
210
- else {
211
- console.log('šŸ“‚ No session state found, starting fresh...');
212
- context = await browser.newContext({
213
- viewport: { width: 1280, height: 800 },
214
- });
215
- }
216
- const page = await context.newPage();
217
- // Set up network interception
218
- page.on('request', captureRequest);
219
- page.on('response', captureResponse);
220
- // Navigate to Teams
221
- console.log('🌐 Navigating to Teams...');
222
- await page.goto('https://teams.microsoft.com', { waitUntil: 'domcontentloaded' });
223
- // Check if authentication is needed
224
- const isAuthenticated = await checkAuthentication(page);
225
- if (!isAuthenticated) {
226
- await waitForAuthentication(page);
227
- // Save session state after successful authentication
228
- console.log('šŸ’¾ Saving session state...');
229
- const state = await context.storageState();
230
- writeSessionState(state);
231
- console.log('āœ… Session state saved!');
232
- }
233
- else {
234
- console.log('āœ… Already authenticated!');
235
- }
236
- // Instructions for the user
237
- console.log('\n' + '='.repeat(60));
238
- console.log('šŸ”¬ RESEARCH MODE ACTIVE');
239
- console.log('='.repeat(60));
240
- console.log('\nThe browser is now monitoring network requests.');
241
- console.log('Try the following to discover API endpoints:\n');
242
- console.log('1. Use the search bar (Cmd/Ctrl+E or click the search icon)');
243
- console.log('2. Search for some text');
244
- console.log('3. Click on search results');
245
- console.log('4. Navigate between chats and channels');
246
- console.log('\nPress Ctrl+C in this terminal when done to save findings.');
247
- console.log('='.repeat(60) + '\n');
248
- // Keep the browser open until interrupted
249
- await new Promise((resolve) => {
250
- process.on('SIGINT', () => {
251
- console.log('\n\nā¹ļø Stopping research...');
252
- resolve();
253
- });
254
- });
255
- // Save research findings FIRST (before browser might close)
256
- await saveFindings();
257
- // Try to save session state (may fail if browser already closed)
258
- try {
259
- console.log('šŸ’¾ Saving final session state...');
260
- const finalState = await context.storageState();
261
- writeSessionState(finalState);
262
- }
263
- catch {
264
- console.log('āš ļø Could not save session state (browser already closed)');
265
- }
266
- }
267
- catch (error) {
268
- // Still try to save findings on error
269
- await saveFindings();
270
- console.error('āŒ Error:', error);
271
- }
272
- finally {
273
- try {
274
- if (context)
275
- await context.close();
276
- }
277
- catch { /* already closed */ }
278
- try {
279
- if (browser)
280
- await browser.close();
281
- }
282
- catch { /* already closed */ }
283
- }
284
- console.log('\nāœ… Research session complete!');
285
- console.log(` Review findings in: ${FINDINGS_PATH}`);
286
- }
287
- main().catch(console.error);
@@ -1,17 +0,0 @@
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
- export {};
@@ -1,317 +0,0 @@
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);
@@ -1,10 +0,0 @@
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 {};