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.
- package/README.md +229 -0
- package/dist/__fixtures__/api-responses.d.ts +228 -0
- package/dist/__fixtures__/api-responses.js +217 -0
- package/dist/api/chatsvc-api.d.ts +171 -0
- package/dist/api/chatsvc-api.js +459 -0
- package/dist/api/csa-api.d.ts +44 -0
- package/dist/api/csa-api.js +148 -0
- package/dist/api/index.d.ts +6 -0
- package/dist/api/index.js +6 -0
- package/dist/api/substrate-api.d.ts +50 -0
- package/dist/api/substrate-api.js +305 -0
- package/dist/auth/crypto.d.ts +32 -0
- package/dist/auth/crypto.js +66 -0
- package/dist/auth/index.d.ts +6 -0
- package/dist/auth/index.js +6 -0
- package/dist/auth/session-store.d.ts +82 -0
- package/dist/auth/session-store.js +136 -0
- package/dist/auth/token-extractor.d.ts +69 -0
- package/dist/auth/token-extractor.js +330 -0
- package/dist/browser/auth.d.ts +43 -0
- package/dist/browser/auth.js +232 -0
- package/dist/browser/context.d.ts +40 -0
- package/dist/browser/context.js +121 -0
- package/dist/browser/session.d.ts +34 -0
- package/dist/browser/session.js +92 -0
- package/dist/constants.d.ts +54 -0
- package/dist/constants.js +72 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +12 -0
- package/dist/research/explore.d.ts +11 -0
- package/dist/research/explore.js +267 -0
- package/dist/research/search-research.d.ts +17 -0
- package/dist/research/search-research.js +317 -0
- package/dist/server.d.ts +64 -0
- package/dist/server.js +291 -0
- package/dist/teams/api-interceptor.d.ts +54 -0
- package/dist/teams/api-interceptor.js +391 -0
- package/dist/teams/direct-api.d.ts +321 -0
- package/dist/teams/direct-api.js +1305 -0
- package/dist/teams/messages.d.ts +14 -0
- package/dist/teams/messages.js +142 -0
- package/dist/teams/search.d.ts +40 -0
- package/dist/teams/search.js +458 -0
- package/dist/test/cli.d.ts +12 -0
- package/dist/test/cli.js +328 -0
- package/dist/test/debug-search.d.ts +10 -0
- package/dist/test/debug-search.js +147 -0
- package/dist/test/manual-test.d.ts +11 -0
- package/dist/test/manual-test.js +160 -0
- package/dist/test/mcp-harness.d.ts +17 -0
- package/dist/test/mcp-harness.js +427 -0
- package/dist/tools/auth-tools.d.ts +26 -0
- package/dist/tools/auth-tools.js +127 -0
- package/dist/tools/index.d.ts +45 -0
- package/dist/tools/index.js +12 -0
- package/dist/tools/message-tools.d.ts +139 -0
- package/dist/tools/message-tools.js +433 -0
- package/dist/tools/people-tools.d.ts +46 -0
- package/dist/tools/people-tools.js +123 -0
- package/dist/tools/registry.d.ts +23 -0
- package/dist/tools/registry.js +61 -0
- package/dist/tools/search-tools.d.ts +79 -0
- package/dist/tools/search-tools.js +168 -0
- package/dist/types/errors.d.ts +58 -0
- package/dist/types/errors.js +132 -0
- package/dist/types/result.d.ts +43 -0
- package/dist/types/result.js +51 -0
- package/dist/types/teams.d.ts +79 -0
- package/dist/types/teams.js +5 -0
- package/dist/utils/api-config.d.ts +66 -0
- package/dist/utils/api-config.js +113 -0
- package/dist/utils/auth-guards.d.ts +29 -0
- package/dist/utils/auth-guards.js +54 -0
- package/dist/utils/http.d.ts +29 -0
- package/dist/utils/http.js +111 -0
- package/dist/utils/parsers.d.ts +187 -0
- package/dist/utils/parsers.js +574 -0
- package/dist/utils/parsers.test.d.ts +7 -0
- package/dist/utils/parsers.test.js +360 -0
- package/package.json +58 -0
|
@@ -0,0 +1,267 @@
|
|
|
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 { fileURLToPath } from 'url';
|
|
15
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
16
|
+
const PROJECT_ROOT = path.resolve(__dirname, '../..');
|
|
17
|
+
const USER_DATA_DIR = path.join(PROJECT_ROOT, '.user-data');
|
|
18
|
+
const SESSION_STATE_PATH = path.join(PROJECT_ROOT, 'session-state.json');
|
|
19
|
+
const FINDINGS_PATH = path.join(PROJECT_ROOT, 'research-findings.json');
|
|
20
|
+
// Track interesting API calls
|
|
21
|
+
const capturedNetworkData = {
|
|
22
|
+
requests: [],
|
|
23
|
+
responses: [],
|
|
24
|
+
};
|
|
25
|
+
// Patterns that indicate search-related API calls
|
|
26
|
+
const SEARCH_PATTERNS = [
|
|
27
|
+
/search/i,
|
|
28
|
+
/query/i,
|
|
29
|
+
/find/i,
|
|
30
|
+
/substrate/i,
|
|
31
|
+
/graph\.microsoft/i,
|
|
32
|
+
/teams.*api/i,
|
|
33
|
+
/chatservice/i,
|
|
34
|
+
/emea\.ng\.msg/i,
|
|
35
|
+
];
|
|
36
|
+
function isInterestingUrl(url) {
|
|
37
|
+
// Skip static assets, telemetry, and auth-related noise
|
|
38
|
+
const skipPatterns = [
|
|
39
|
+
/\.(js|css|png|jpg|jpeg|gif|svg|woff|woff2|ttf|ico)(\?|$)/i,
|
|
40
|
+
/telemetry/i,
|
|
41
|
+
/analytics/i,
|
|
42
|
+
/logging/i,
|
|
43
|
+
/beacon/i,
|
|
44
|
+
/\.clarity\./i,
|
|
45
|
+
/fonts\./i,
|
|
46
|
+
/static/i,
|
|
47
|
+
];
|
|
48
|
+
if (skipPatterns.some(pattern => pattern.test(url))) {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
// Include if it matches search patterns or is a potential API call
|
|
52
|
+
return SEARCH_PATTERNS.some(pattern => pattern.test(url)) ||
|
|
53
|
+
url.includes('/api/') ||
|
|
54
|
+
url.includes('/v1/') ||
|
|
55
|
+
url.includes('/v2/');
|
|
56
|
+
}
|
|
57
|
+
async function captureRequest(request) {
|
|
58
|
+
const url = request.url();
|
|
59
|
+
if (!isInterestingUrl(url)) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
const captured = {
|
|
63
|
+
url,
|
|
64
|
+
method: request.method(),
|
|
65
|
+
headers: request.headers(),
|
|
66
|
+
postData: request.postData() ?? undefined,
|
|
67
|
+
timestamp: new Date().toISOString(),
|
|
68
|
+
resourceType: request.resourceType(),
|
|
69
|
+
};
|
|
70
|
+
capturedNetworkData.requests.push(captured);
|
|
71
|
+
console.log(`\nš¤ REQUEST: ${request.method()} ${url}`);
|
|
72
|
+
if (captured.postData) {
|
|
73
|
+
try {
|
|
74
|
+
const parsed = JSON.parse(captured.postData);
|
|
75
|
+
console.log(' Body:', JSON.stringify(parsed, null, 2).substring(0, 500));
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
console.log(' Body:', captured.postData.substring(0, 200));
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
async function captureResponse(response) {
|
|
83
|
+
const url = response.url();
|
|
84
|
+
if (!isInterestingUrl(url)) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
let body;
|
|
88
|
+
try {
|
|
89
|
+
const contentType = response.headers()['content-type'] || '';
|
|
90
|
+
if (contentType.includes('application/json')) {
|
|
91
|
+
body = await response.text();
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
// Response body may not be available
|
|
96
|
+
}
|
|
97
|
+
const captured = {
|
|
98
|
+
url,
|
|
99
|
+
status: response.status(),
|
|
100
|
+
statusText: response.statusText(),
|
|
101
|
+
headers: response.headers(),
|
|
102
|
+
body,
|
|
103
|
+
timestamp: new Date().toISOString(),
|
|
104
|
+
};
|
|
105
|
+
capturedNetworkData.responses.push(captured);
|
|
106
|
+
console.log(`\nš„ RESPONSE: ${response.status()} ${url}`);
|
|
107
|
+
if (body) {
|
|
108
|
+
try {
|
|
109
|
+
const parsed = JSON.parse(body);
|
|
110
|
+
console.log(' Body preview:', JSON.stringify(parsed, null, 2).substring(0, 500));
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
console.log(' Body preview:', body.substring(0, 200));
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
async function saveFindings() {
|
|
118
|
+
// Deduplicate and summarise findings
|
|
119
|
+
const uniqueEndpoints = new Map();
|
|
120
|
+
for (const req of capturedNetworkData.requests) {
|
|
121
|
+
const key = `${req.method}:${new URL(req.url).pathname}`;
|
|
122
|
+
if (!uniqueEndpoints.has(key)) {
|
|
123
|
+
uniqueEndpoints.set(key, req);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
const findings = {
|
|
127
|
+
discoveredAt: new Date().toISOString(),
|
|
128
|
+
uniqueEndpoints: Array.from(uniqueEndpoints.values()).map(req => ({
|
|
129
|
+
method: req.method,
|
|
130
|
+
url: req.url,
|
|
131
|
+
hasPostData: !!req.postData,
|
|
132
|
+
postDataSample: req.postData?.substring(0, 500),
|
|
133
|
+
})),
|
|
134
|
+
allRequests: capturedNetworkData.requests,
|
|
135
|
+
allResponses: capturedNetworkData.responses,
|
|
136
|
+
};
|
|
137
|
+
fs.writeFileSync(FINDINGS_PATH, JSON.stringify(findings, null, 2));
|
|
138
|
+
console.log(`\nš¾ Findings saved to ${FINDINGS_PATH}`);
|
|
139
|
+
}
|
|
140
|
+
async function checkAuthentication(page) {
|
|
141
|
+
const url = page.url();
|
|
142
|
+
// Check if we're on a login page
|
|
143
|
+
if (url.includes('login.microsoftonline.com') ||
|
|
144
|
+
url.includes('login.live.com') ||
|
|
145
|
+
url.includes('login.microsoft.com')) {
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
// Check if we're on the Teams app
|
|
149
|
+
if (url.includes('teams.microsoft.com')) {
|
|
150
|
+
// Look for indicators that we're logged in
|
|
151
|
+
try {
|
|
152
|
+
// Wait briefly for the app to load
|
|
153
|
+
await page.waitForTimeout(2000);
|
|
154
|
+
// Check for common authenticated elements
|
|
155
|
+
const hasAppBar = await page.locator('[data-tid="app-bar"]').count() > 0;
|
|
156
|
+
const hasSearchBox = await page.locator('[data-tid="search-box"]').count() > 0 ||
|
|
157
|
+
await page.locator('input[placeholder*="Search"]').count() > 0;
|
|
158
|
+
return hasAppBar || hasSearchBox;
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
async function waitForAuthentication(page) {
|
|
167
|
+
console.log('\nš Please log in to Microsoft Teams in the browser window...');
|
|
168
|
+
console.log(' The script will continue once you are authenticated.\n');
|
|
169
|
+
// Poll for authentication
|
|
170
|
+
while (true) {
|
|
171
|
+
if (await checkAuthentication(page)) {
|
|
172
|
+
console.log('ā
Authentication detected!');
|
|
173
|
+
break;
|
|
174
|
+
}
|
|
175
|
+
await page.waitForTimeout(2000);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
async function main() {
|
|
179
|
+
console.log('š Teams Research Script');
|
|
180
|
+
console.log('========================\n');
|
|
181
|
+
console.log('This script will help discover how Teams web app works.\n');
|
|
182
|
+
// Ensure user data directory exists
|
|
183
|
+
if (!fs.existsSync(USER_DATA_DIR)) {
|
|
184
|
+
fs.mkdirSync(USER_DATA_DIR, { recursive: true });
|
|
185
|
+
}
|
|
186
|
+
let browser;
|
|
187
|
+
let context;
|
|
188
|
+
try {
|
|
189
|
+
// Launch browser with persistent context
|
|
190
|
+
browser = await chromium.launch({
|
|
191
|
+
headless: false, // Must be visible for manual login
|
|
192
|
+
});
|
|
193
|
+
// Check if we have saved session state
|
|
194
|
+
const hasSessionState = fs.existsSync(SESSION_STATE_PATH);
|
|
195
|
+
if (hasSessionState) {
|
|
196
|
+
console.log('š Found existing session state, attempting to restore...');
|
|
197
|
+
context = await browser.newContext({
|
|
198
|
+
storageState: SESSION_STATE_PATH,
|
|
199
|
+
viewport: { width: 1280, height: 800 },
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
console.log('š No session state found, starting fresh...');
|
|
204
|
+
context = await browser.newContext({
|
|
205
|
+
viewport: { width: 1280, height: 800 },
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
const page = await context.newPage();
|
|
209
|
+
// Set up network interception
|
|
210
|
+
page.on('request', captureRequest);
|
|
211
|
+
page.on('response', captureResponse);
|
|
212
|
+
// Navigate to Teams
|
|
213
|
+
console.log('š Navigating to Teams...');
|
|
214
|
+
await page.goto('https://teams.microsoft.com', { waitUntil: 'domcontentloaded' });
|
|
215
|
+
// Check if authentication is needed
|
|
216
|
+
const isAuthenticated = await checkAuthentication(page);
|
|
217
|
+
if (!isAuthenticated) {
|
|
218
|
+
await waitForAuthentication(page);
|
|
219
|
+
// Save session state after successful authentication
|
|
220
|
+
console.log('š¾ Saving session state...');
|
|
221
|
+
await context.storageState({ path: SESSION_STATE_PATH });
|
|
222
|
+
console.log('ā
Session state saved!');
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
console.log('ā
Already authenticated!');
|
|
226
|
+
}
|
|
227
|
+
// Instructions for the user
|
|
228
|
+
console.log('\n' + '='.repeat(60));
|
|
229
|
+
console.log('š¬ RESEARCH MODE ACTIVE');
|
|
230
|
+
console.log('='.repeat(60));
|
|
231
|
+
console.log('\nThe browser is now monitoring network requests.');
|
|
232
|
+
console.log('Try the following to discover API endpoints:\n');
|
|
233
|
+
console.log('1. Use the search bar (Cmd/Ctrl+E or click the search icon)');
|
|
234
|
+
console.log('2. Search for some text');
|
|
235
|
+
console.log('3. Click on search results');
|
|
236
|
+
console.log('4. Navigate between chats and channels');
|
|
237
|
+
console.log('\nPress Ctrl+C in this terminal when done to save findings.');
|
|
238
|
+
console.log('='.repeat(60) + '\n');
|
|
239
|
+
// Keep the browser open until interrupted
|
|
240
|
+
await new Promise((resolve) => {
|
|
241
|
+
process.on('SIGINT', () => {
|
|
242
|
+
console.log('\n\nā¹ļø Stopping research...');
|
|
243
|
+
resolve();
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
// Save session state before closing
|
|
247
|
+
console.log('š¾ Saving final session state...');
|
|
248
|
+
await context.storageState({ path: SESSION_STATE_PATH });
|
|
249
|
+
// Save research findings
|
|
250
|
+
await saveFindings();
|
|
251
|
+
}
|
|
252
|
+
catch (error) {
|
|
253
|
+
console.error('ā Error:', error);
|
|
254
|
+
throw error;
|
|
255
|
+
}
|
|
256
|
+
finally {
|
|
257
|
+
if (context) {
|
|
258
|
+
await context.close();
|
|
259
|
+
}
|
|
260
|
+
if (browser) {
|
|
261
|
+
await browser.close();
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
console.log('\nā
Research session complete!');
|
|
265
|
+
console.log(` Review findings in: ${FINDINGS_PATH}`);
|
|
266
|
+
}
|
|
267
|
+
main().catch(console.error);
|
|
@@ -0,0 +1,17 @@
|
|
|
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 {};
|
|
@@ -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);
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Server implementation for Teams search.
|
|
3
|
+
* Exposes tools and resources for interacting with Microsoft Teams.
|
|
4
|
+
*/
|
|
5
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
6
|
+
import { type BrowserManager } from './browser/context.js';
|
|
7
|
+
/**
|
|
8
|
+
* MCP Server for Teams integration.
|
|
9
|
+
*
|
|
10
|
+
* Encapsulates all server state to allow multiple instances.
|
|
11
|
+
*/
|
|
12
|
+
export declare class TeamsServer {
|
|
13
|
+
private browserManager;
|
|
14
|
+
private isInitialised;
|
|
15
|
+
/**
|
|
16
|
+
* Returns a standard MCP error response.
|
|
17
|
+
*/
|
|
18
|
+
private formatError;
|
|
19
|
+
/**
|
|
20
|
+
* Returns a standard MCP success response.
|
|
21
|
+
*/
|
|
22
|
+
private formatSuccess;
|
|
23
|
+
/**
|
|
24
|
+
* Gets the current browser manager.
|
|
25
|
+
*/
|
|
26
|
+
getBrowserManager(): BrowserManager | null;
|
|
27
|
+
/**
|
|
28
|
+
* Sets the browser manager.
|
|
29
|
+
*/
|
|
30
|
+
setBrowserManager(manager: BrowserManager): void;
|
|
31
|
+
/**
|
|
32
|
+
* Resets browser state.
|
|
33
|
+
*/
|
|
34
|
+
resetBrowserState(): void;
|
|
35
|
+
/**
|
|
36
|
+
* Marks the server as initialised.
|
|
37
|
+
*/
|
|
38
|
+
markInitialised(): void;
|
|
39
|
+
/**
|
|
40
|
+
* Checks if the server is initialised.
|
|
41
|
+
*/
|
|
42
|
+
isInitialisedState(): boolean;
|
|
43
|
+
/**
|
|
44
|
+
* Ensures the browser is running and authenticated.
|
|
45
|
+
*/
|
|
46
|
+
ensureBrowser(headless?: boolean): Promise<BrowserManager>;
|
|
47
|
+
/**
|
|
48
|
+
* Cleans up browser resources.
|
|
49
|
+
*/
|
|
50
|
+
cleanup(): Promise<void>;
|
|
51
|
+
/**
|
|
52
|
+
* Creates and configures the MCP server.
|
|
53
|
+
*/
|
|
54
|
+
createServer(): Promise<Server>;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Creates and runs the MCP server.
|
|
58
|
+
* Exported for backward compatibility.
|
|
59
|
+
*/
|
|
60
|
+
export declare function createServer(): Promise<Server>;
|
|
61
|
+
/**
|
|
62
|
+
* Runs the server with stdio transport.
|
|
63
|
+
*/
|
|
64
|
+
export declare function runServer(): Promise<void>;
|