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
package/dist/test/cli.js
ADDED
|
@@ -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,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 {};
|