msteams-mcp 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/README.md +229 -0
  2. package/dist/__fixtures__/api-responses.d.ts +228 -0
  3. package/dist/__fixtures__/api-responses.js +217 -0
  4. package/dist/api/chatsvc-api.d.ts +171 -0
  5. package/dist/api/chatsvc-api.js +459 -0
  6. package/dist/api/csa-api.d.ts +44 -0
  7. package/dist/api/csa-api.js +148 -0
  8. package/dist/api/index.d.ts +6 -0
  9. package/dist/api/index.js +6 -0
  10. package/dist/api/substrate-api.d.ts +50 -0
  11. package/dist/api/substrate-api.js +305 -0
  12. package/dist/auth/crypto.d.ts +32 -0
  13. package/dist/auth/crypto.js +66 -0
  14. package/dist/auth/index.d.ts +6 -0
  15. package/dist/auth/index.js +6 -0
  16. package/dist/auth/session-store.d.ts +82 -0
  17. package/dist/auth/session-store.js +136 -0
  18. package/dist/auth/token-extractor.d.ts +69 -0
  19. package/dist/auth/token-extractor.js +330 -0
  20. package/dist/browser/auth.d.ts +43 -0
  21. package/dist/browser/auth.js +232 -0
  22. package/dist/browser/context.d.ts +40 -0
  23. package/dist/browser/context.js +121 -0
  24. package/dist/browser/session.d.ts +34 -0
  25. package/dist/browser/session.js +92 -0
  26. package/dist/constants.d.ts +54 -0
  27. package/dist/constants.js +72 -0
  28. package/dist/index.d.ts +8 -0
  29. package/dist/index.js +12 -0
  30. package/dist/research/explore.d.ts +11 -0
  31. package/dist/research/explore.js +267 -0
  32. package/dist/research/search-research.d.ts +17 -0
  33. package/dist/research/search-research.js +317 -0
  34. package/dist/server.d.ts +64 -0
  35. package/dist/server.js +291 -0
  36. package/dist/teams/api-interceptor.d.ts +54 -0
  37. package/dist/teams/api-interceptor.js +391 -0
  38. package/dist/teams/direct-api.d.ts +321 -0
  39. package/dist/teams/direct-api.js +1305 -0
  40. package/dist/teams/messages.d.ts +14 -0
  41. package/dist/teams/messages.js +142 -0
  42. package/dist/teams/search.d.ts +40 -0
  43. package/dist/teams/search.js +458 -0
  44. package/dist/test/cli.d.ts +12 -0
  45. package/dist/test/cli.js +328 -0
  46. package/dist/test/debug-search.d.ts +10 -0
  47. package/dist/test/debug-search.js +147 -0
  48. package/dist/test/manual-test.d.ts +11 -0
  49. package/dist/test/manual-test.js +160 -0
  50. package/dist/test/mcp-harness.d.ts +17 -0
  51. package/dist/test/mcp-harness.js +427 -0
  52. package/dist/tools/auth-tools.d.ts +26 -0
  53. package/dist/tools/auth-tools.js +127 -0
  54. package/dist/tools/index.d.ts +45 -0
  55. package/dist/tools/index.js +12 -0
  56. package/dist/tools/message-tools.d.ts +139 -0
  57. package/dist/tools/message-tools.js +433 -0
  58. package/dist/tools/people-tools.d.ts +46 -0
  59. package/dist/tools/people-tools.js +123 -0
  60. package/dist/tools/registry.d.ts +23 -0
  61. package/dist/tools/registry.js +61 -0
  62. package/dist/tools/search-tools.d.ts +79 -0
  63. package/dist/tools/search-tools.js +168 -0
  64. package/dist/types/errors.d.ts +58 -0
  65. package/dist/types/errors.js +132 -0
  66. package/dist/types/result.d.ts +43 -0
  67. package/dist/types/result.js +51 -0
  68. package/dist/types/teams.d.ts +79 -0
  69. package/dist/types/teams.js +5 -0
  70. package/dist/utils/api-config.d.ts +66 -0
  71. package/dist/utils/api-config.js +113 -0
  72. package/dist/utils/auth-guards.d.ts +29 -0
  73. package/dist/utils/auth-guards.js +54 -0
  74. package/dist/utils/http.d.ts +29 -0
  75. package/dist/utils/http.js +111 -0
  76. package/dist/utils/parsers.d.ts +187 -0
  77. package/dist/utils/parsers.js +574 -0
  78. package/dist/utils/parsers.test.d.ts +7 -0
  79. package/dist/utils/parsers.test.js +360 -0
  80. package/package.json +58 -0
@@ -0,0 +1,427 @@
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
+ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
18
+ import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js';
19
+ import { createServer } from '../server.js';
20
+ // Shortcuts map command names to tool names and parameter mappings
21
+ const SHORTCUTS = {
22
+ search: { tool: 'teams_search', primaryArg: 'query' },
23
+ status: { tool: 'teams_status' },
24
+ send: { tool: 'teams_send_message', primaryArg: 'content' },
25
+ reply: { tool: 'teams_reply_to_thread', primaryArg: 'content' },
26
+ me: { tool: 'teams_get_me' },
27
+ people: { tool: 'teams_search_people', primaryArg: 'query' },
28
+ favorites: { tool: 'teams_get_favorites' },
29
+ save: { tool: 'teams_save_message' },
30
+ unsave: { tool: 'teams_unsave_message' },
31
+ thread: { tool: 'teams_get_thread' },
32
+ login: { tool: 'teams_login' },
33
+ contacts: { tool: 'teams_get_frequent_contacts' },
34
+ channel: { tool: 'teams_find_channel', primaryArg: 'query' },
35
+ chat: { tool: 'teams_get_chat', primaryArg: 'userId' },
36
+ };
37
+ // Map CLI flags to tool parameter names
38
+ const FLAG_MAPPINGS = {
39
+ '--to': 'conversationId',
40
+ '--message': 'messageId',
41
+ '--reply': 'replyToMessageId',
42
+ '--replyTo': 'replyToMessageId',
43
+ '--from': 'from',
44
+ '--size': 'size',
45
+ '--limit': 'limit',
46
+ '--query': 'query',
47
+ '--content': 'content',
48
+ '--force': 'forceNew',
49
+ '--user': 'userId',
50
+ '--userId': 'userId',
51
+ };
52
+ function parseArgs() {
53
+ const args = process.argv.slice(2);
54
+ const result = {
55
+ command: 'list',
56
+ toolName: null,
57
+ primaryArg: null,
58
+ args: {},
59
+ json: false,
60
+ };
61
+ let i = 0;
62
+ // First pass: find the command (first non-flag argument)
63
+ while (i < args.length) {
64
+ const arg = args[i];
65
+ if (arg === '--json') {
66
+ result.json = true;
67
+ i++;
68
+ continue;
69
+ }
70
+ if (arg.startsWith('--')) {
71
+ // Skip flag and its value
72
+ i += 2;
73
+ continue;
74
+ }
75
+ // Found the command
76
+ result.command = arg;
77
+ i++;
78
+ break;
79
+ }
80
+ // Check if it's a shortcut
81
+ const shortcut = SHORTCUTS[result.command];
82
+ if (shortcut) {
83
+ result.toolName = shortcut.tool;
84
+ // Look for primary argument (next non-flag arg)
85
+ while (i < args.length) {
86
+ const arg = args[i];
87
+ if (!arg.startsWith('--')) {
88
+ if (shortcut.primaryArg) {
89
+ result.args[shortcut.primaryArg] = arg;
90
+ }
91
+ result.primaryArg = arg;
92
+ i++;
93
+ break;
94
+ }
95
+ i++;
96
+ }
97
+ }
98
+ else if (result.command !== 'list') {
99
+ // Treat as a tool name (add teams_ prefix if not present)
100
+ result.toolName = result.command.startsWith('teams_')
101
+ ? result.command
102
+ : `teams_${result.command}`;
103
+ }
104
+ // Second pass: collect all --key value pairs
105
+ for (let j = 0; j < args.length; j++) {
106
+ const arg = args[j];
107
+ if (arg === '--json') {
108
+ result.json = true;
109
+ continue;
110
+ }
111
+ if (arg.startsWith('--') && args[j + 1] !== undefined) {
112
+ const key = FLAG_MAPPINGS[arg] || arg.slice(2); // Use mapping or strip --
113
+ let value = args[j + 1];
114
+ // Try to parse as number or boolean
115
+ if (value === 'true')
116
+ value = true;
117
+ else if (value === 'false')
118
+ value = false;
119
+ else if (/^\d+$/.test(value))
120
+ value = parseInt(value, 10);
121
+ result.args[key] = value;
122
+ j++; // Skip the value
123
+ }
124
+ }
125
+ return result;
126
+ }
127
+ function log(message) {
128
+ console.log(message);
129
+ }
130
+ function logSection(title) {
131
+ console.log('\n' + '─'.repeat(50));
132
+ console.log(` ${title}`);
133
+ console.log('─'.repeat(50));
134
+ }
135
+ async function createTestClient() {
136
+ // Create the MCP server
137
+ const server = await createServer();
138
+ // Create linked in-memory transports
139
+ const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
140
+ // Connect the server to its transport
141
+ await server.connect(serverTransport);
142
+ // Create and connect the client
143
+ const client = new Client({ name: 'mcp-test-harness', version: '1.0.0' }, { capabilities: {} });
144
+ await client.connect(clientTransport);
145
+ const cleanup = async () => {
146
+ await client.close();
147
+ await server.close();
148
+ };
149
+ return { client, cleanup };
150
+ }
151
+ async function listTools(client, json) {
152
+ if (!json) {
153
+ logSection('Available Tools');
154
+ }
155
+ const result = await client.listTools();
156
+ if (json) {
157
+ console.log(JSON.stringify(result, null, 2));
158
+ return;
159
+ }
160
+ log(`Found ${result.tools.length} tools:\n`);
161
+ for (const tool of result.tools) {
162
+ log(`📦 ${tool.name}`);
163
+ log(` ${tool.description}`);
164
+ const schema = tool.inputSchema;
165
+ if (schema.properties) {
166
+ const props = Object.entries(schema.properties);
167
+ const required = new Set(schema.required ?? []);
168
+ for (const [name, prop] of props) {
169
+ const propObj = prop;
170
+ const reqMark = required.has(name) ? ' (required)' : '';
171
+ log(` - ${name}: ${propObj.type ?? 'any'}${reqMark}`);
172
+ if (propObj.description) {
173
+ log(` ${propObj.description}`);
174
+ }
175
+ }
176
+ }
177
+ log('');
178
+ }
179
+ // Show available shortcuts
180
+ log('Shortcuts:');
181
+ for (const [shortcut, config] of Object.entries(SHORTCUTS)) {
182
+ const primaryNote = config.primaryArg ? ` <${config.primaryArg}>` : '';
183
+ log(` ${shortcut}${primaryNote} → ${config.tool}`);
184
+ }
185
+ log('');
186
+ log('Any unrecognised command is treated as a tool name.');
187
+ log('Use --key value for parameters, e.g.: teams_find_channel --query "name"');
188
+ }
189
+ async function callTool(client, toolName, args, json) {
190
+ // Verify the tool exists
191
+ const tools = await client.listTools();
192
+ const tool = tools.tools.find(t => t.name === toolName);
193
+ if (!tool) {
194
+ console.error(`❌ Unknown tool: ${toolName}`);
195
+ console.error('');
196
+ console.error('Available tools:');
197
+ for (const t of tools.tools) {
198
+ console.error(` - ${t.name}`);
199
+ }
200
+ process.exit(1);
201
+ }
202
+ if (!json) {
203
+ logSection(`Calling: ${toolName}`);
204
+ if (Object.keys(args).length > 0) {
205
+ log(`Arguments: ${JSON.stringify(args)}\n`);
206
+ }
207
+ }
208
+ const result = await client.callTool({ name: toolName, arguments: args });
209
+ if (json) {
210
+ console.log(JSON.stringify(result, null, 2));
211
+ return;
212
+ }
213
+ // Pretty-print the result
214
+ const content = result.content;
215
+ const textContent = content.find(c => c.type === 'text');
216
+ if (textContent?.text) {
217
+ try {
218
+ const response = JSON.parse(textContent.text);
219
+ prettyPrintResponse(response, toolName);
220
+ }
221
+ catch {
222
+ // Not JSON, just print as-is
223
+ log(textContent.text);
224
+ }
225
+ }
226
+ else {
227
+ log('(No text content in response)');
228
+ }
229
+ }
230
+ /**
231
+ * Pretty-prints a tool response based on common patterns
232
+ */
233
+ function prettyPrintResponse(response, toolName) {
234
+ // Check for error
235
+ if (response.success === false) {
236
+ log(`❌ Failed: ${response.error || 'Unknown error'}`);
237
+ if (response.code)
238
+ log(` Code: ${response.code}`);
239
+ if (response.suggestion)
240
+ log(` Suggestion: ${response.suggestion}`);
241
+ return;
242
+ }
243
+ log('✅ Success\n');
244
+ // Handle common response shapes
245
+ if (response.results && Array.isArray(response.results)) {
246
+ printResultsList(response.results, response);
247
+ }
248
+ else if (response.favorites && Array.isArray(response.favorites)) {
249
+ printFavoritesList(response.favorites);
250
+ }
251
+ else if (response.messages && Array.isArray(response.messages)) {
252
+ printMessagesList(response.messages);
253
+ }
254
+ else if (response.contacts && Array.isArray(response.contacts)) {
255
+ printContactsList(response.contacts);
256
+ }
257
+ else if (response.profile) {
258
+ printProfile(response.profile);
259
+ }
260
+ else {
261
+ // Generic output for other responses
262
+ printGenericResponse(response);
263
+ }
264
+ // Print pagination if present
265
+ if (response.pagination) {
266
+ const p = response.pagination;
267
+ log(`\nPagination: from=${p.from}, size=${p.size}, returned=${p.returned}`);
268
+ if (p.total !== undefined)
269
+ log(`Total available: ${p.total}`);
270
+ if (p.hasMore)
271
+ log(`More results available (use --from ${p.nextFrom})`);
272
+ }
273
+ }
274
+ function printResultsList(results, response) {
275
+ log(`Found ${response.resultCount ?? results.length} results:\n`);
276
+ for (let i = 0; i < results.length; i++) {
277
+ const r = results[i];
278
+ const num = (response.pagination?.from ?? 0) + i + 1;
279
+ // Try to extract meaningful content
280
+ const content = String(r.content ?? r.displayName ?? r.name ?? '').substring(0, 100).replace(/\n/g, ' ');
281
+ log(`${num}. ${content}${content.length >= 100 ? '...' : ''}`);
282
+ // Print common fields
283
+ const sender = extractSenderName(r.sender);
284
+ if (sender)
285
+ log(` From: ${sender}`);
286
+ if (r.email)
287
+ log(` Email: ${r.email}`);
288
+ if (r.teamName)
289
+ log(` Team: ${r.teamName}`);
290
+ if (r.channelName && r.channelName !== r.teamName)
291
+ log(` Channel: ${r.channelName}`);
292
+ if (r.timestamp)
293
+ log(` Time: ${r.timestamp}`);
294
+ if (r.conversationId)
295
+ log(` ConversationId: ${r.conversationId}`);
296
+ if (r.jobTitle)
297
+ log(` Title: ${r.jobTitle}`);
298
+ if (r.department)
299
+ log(` Dept: ${r.department}`);
300
+ if (r.mri)
301
+ log(` MRI: ${r.mri}`);
302
+ log('');
303
+ }
304
+ }
305
+ function printFavoritesList(favorites) {
306
+ log(`Found ${favorites.length} favourites:\n`);
307
+ for (const f of favorites) {
308
+ const fav = f;
309
+ const typeLabel = fav.conversationType ? ` [${fav.conversationType}]` : '';
310
+ const nameLabel = fav.displayName || '(unnamed)';
311
+ log(`⭐ ${nameLabel}${typeLabel}`);
312
+ log(` ID: ${fav.conversationId}`);
313
+ }
314
+ }
315
+ function printMessagesList(messages) {
316
+ log(`Got ${messages.length} messages:\n`);
317
+ for (const msg of messages) {
318
+ const m = msg;
319
+ const preview = String(m.content ?? '').substring(0, 100).replace(/\n/g, ' ');
320
+ const sender = m.sender?.displayName ||
321
+ m.sender?.mri || 'Unknown';
322
+ const time = m.timestamp ? new Date(m.timestamp).toLocaleString() : '';
323
+ const fromMe = m.isFromMe ? ' (you)' : '';
324
+ log(`📝 ${sender}${fromMe} - ${time}`);
325
+ log(` ${preview}${String(m.content ?? '').length > 100 ? '...' : ''}`);
326
+ log('');
327
+ }
328
+ }
329
+ function printContactsList(contacts) {
330
+ log(`Found ${contacts.length} contacts:\n`);
331
+ for (const c of contacts) {
332
+ const contact = c;
333
+ log(`👤 ${contact.displayName}`);
334
+ if (contact.email)
335
+ log(` Email: ${contact.email}`);
336
+ if (contact.jobTitle)
337
+ log(` Title: ${contact.jobTitle}`);
338
+ if (contact.department)
339
+ log(` Dept: ${contact.department}`);
340
+ log('');
341
+ }
342
+ }
343
+ function printProfile(profile) {
344
+ log('👤 Profile:\n');
345
+ for (const [key, value] of Object.entries(profile)) {
346
+ if (value !== null && value !== undefined) {
347
+ log(` ${key}: ${value}`);
348
+ }
349
+ }
350
+ }
351
+ function printGenericResponse(response) {
352
+ // Filter out success flag and print remaining fields
353
+ for (const [key, value] of Object.entries(response)) {
354
+ if (key === 'success')
355
+ continue;
356
+ if (typeof value === 'object' && value !== null) {
357
+ log(`${key}: ${JSON.stringify(value, null, 2)}`);
358
+ }
359
+ else {
360
+ log(`${key}: ${value}`);
361
+ }
362
+ }
363
+ }
364
+ function extractSenderName(sender) {
365
+ if (!sender)
366
+ return null;
367
+ if (typeof sender === 'string')
368
+ return sender;
369
+ if (typeof sender === 'object') {
370
+ const s = sender;
371
+ // Handle { EmailAddress: { Name: string } } structure
372
+ if (s.EmailAddress && typeof s.EmailAddress === 'object') {
373
+ const email = s.EmailAddress;
374
+ if (email.Name)
375
+ return String(email.Name);
376
+ if (email.Address)
377
+ return String(email.Address);
378
+ }
379
+ // Handle { name: string } or { displayName: string } structure
380
+ if (s.displayName)
381
+ return String(s.displayName);
382
+ if (s.name)
383
+ return String(s.name);
384
+ if (s.Name)
385
+ return String(s.Name);
386
+ }
387
+ return null;
388
+ }
389
+ async function main() {
390
+ const parsed = parseArgs();
391
+ if (!parsed.json) {
392
+ console.log('\n🧪 MCP Protocol Test Harness');
393
+ console.log('============================');
394
+ }
395
+ let cleanup = null;
396
+ try {
397
+ const { client, cleanup: cleanupFn } = await createTestClient();
398
+ cleanup = cleanupFn;
399
+ if (!parsed.json) {
400
+ log('\n✅ Connected to MCP server via in-memory transport');
401
+ }
402
+ if (parsed.command === 'list' || !parsed.toolName) {
403
+ await listTools(client, parsed.json);
404
+ }
405
+ else {
406
+ await callTool(client, parsed.toolName, parsed.args, parsed.json);
407
+ }
408
+ if (!parsed.json) {
409
+ logSection('Complete');
410
+ log('MCP protocol test finished successfully.');
411
+ }
412
+ }
413
+ catch (error) {
414
+ console.error('\n❌ Error:', error instanceof Error ? error.message : String(error));
415
+ if (error instanceof Error && error.stack) {
416
+ console.error('\nStack trace:');
417
+ console.error(error.stack);
418
+ }
419
+ process.exit(1);
420
+ }
421
+ finally {
422
+ if (cleanup) {
423
+ await cleanup();
424
+ }
425
+ }
426
+ }
427
+ main();
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Authentication-related tool handlers.
3
+ */
4
+ import { z } from 'zod';
5
+ import type { RegisteredTool } from './index.js';
6
+ export declare const LoginInputSchema: z.ZodObject<{
7
+ forceNew: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
8
+ }, "strip", z.ZodTypeAny, {
9
+ forceNew: boolean;
10
+ }, {
11
+ forceNew?: boolean | undefined;
12
+ }>;
13
+ export declare const loginTool: RegisteredTool<typeof LoginInputSchema>;
14
+ export declare const statusTool: RegisteredTool<z.ZodObject<Record<string, never>>>;
15
+ /** All auth-related tools. */
16
+ export declare const authTools: (RegisteredTool<z.ZodObject<Record<string, never>, z.UnknownKeysParam, z.ZodTypeAny, {
17
+ [x: string]: never;
18
+ }, {
19
+ [x: string]: never;
20
+ }>> | RegisteredTool<z.ZodObject<{
21
+ forceNew: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
22
+ }, "strip", z.ZodTypeAny, {
23
+ forceNew: boolean;
24
+ }, {
25
+ forceNew?: boolean | undefined;
26
+ }>>)[];
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Authentication-related tool handlers.
3
+ */
4
+ import { z } from 'zod';
5
+ import { hasSessionState, isSessionLikelyExpired, clearSessionState, } from '../auth/session-store.js';
6
+ import { getSubstrateTokenStatus, extractMessageAuth, extractCsaToken, clearTokenCache, } from '../auth/token-extractor.js';
7
+ import { createBrowserContext, closeBrowser } from '../browser/context.js';
8
+ import { ensureAuthenticated, forceNewLogin, getAuthStatus } from '../browser/auth.js';
9
+ // ─────────────────────────────────────────────────────────────────────────────
10
+ // Schemas
11
+ // ─────────────────────────────────────────────────────────────────────────────
12
+ export const LoginInputSchema = z.object({
13
+ forceNew: z.boolean().optional().default(false),
14
+ });
15
+ // ─────────────────────────────────────────────────────────────────────────────
16
+ // Tool Definitions
17
+ // ─────────────────────────────────────────────────────────────────────────────
18
+ const loginToolDefinition = {
19
+ name: 'teams_login',
20
+ description: 'Trigger manual login flow for Microsoft Teams. Use this if the session has expired or you need to switch accounts.',
21
+ inputSchema: {
22
+ type: 'object',
23
+ properties: {
24
+ forceNew: {
25
+ type: 'boolean',
26
+ description: 'Force a new login even if a session exists (default: false)',
27
+ },
28
+ },
29
+ },
30
+ };
31
+ const statusToolDefinition = {
32
+ name: 'teams_status',
33
+ description: 'Check the current authentication status and session state.',
34
+ inputSchema: {
35
+ type: 'object',
36
+ properties: {},
37
+ },
38
+ };
39
+ // ─────────────────────────────────────────────────────────────────────────────
40
+ // Handlers
41
+ // ─────────────────────────────────────────────────────────────────────────────
42
+ async function handleLogin(input, ctx) {
43
+ // Close existing browser if any
44
+ const existingManager = ctx.server.getBrowserManager();
45
+ if (existingManager) {
46
+ await closeBrowser(existingManager, !input.forceNew);
47
+ ctx.server.resetBrowserState();
48
+ }
49
+ if (input.forceNew) {
50
+ clearSessionState();
51
+ clearTokenCache();
52
+ }
53
+ const browserManager = await createBrowserContext({ headless: false });
54
+ ctx.server.setBrowserManager(browserManager);
55
+ try {
56
+ if (input.forceNew) {
57
+ await forceNewLogin(browserManager.page, browserManager.context, (msg) => console.error(`[login] ${msg}`));
58
+ }
59
+ else {
60
+ await ensureAuthenticated(browserManager.page, browserManager.context, (msg) => console.error(`[login] ${msg}`));
61
+ }
62
+ }
63
+ finally {
64
+ // Close browser after login - we only need the saved session/tokens
65
+ await closeBrowser(browserManager, true);
66
+ ctx.server.resetBrowserState();
67
+ }
68
+ ctx.server.markInitialised();
69
+ return {
70
+ success: true,
71
+ data: {
72
+ message: 'Login completed successfully. Session has been saved.',
73
+ },
74
+ };
75
+ }
76
+ async function handleStatus(_input, ctx) {
77
+ const sessionExists = hasSessionState();
78
+ const sessionExpired = isSessionLikelyExpired();
79
+ const tokenStatus = getSubstrateTokenStatus();
80
+ const messageAuth = extractMessageAuth();
81
+ const csaToken = extractCsaToken();
82
+ let authStatus = null;
83
+ const browserManager = ctx.server.getBrowserManager();
84
+ if (browserManager && ctx.server.isInitialisedState()) {
85
+ authStatus = await getAuthStatus(browserManager.page);
86
+ }
87
+ return {
88
+ success: true,
89
+ data: {
90
+ directApi: {
91
+ available: tokenStatus.hasToken,
92
+ expiresAt: tokenStatus.expiresAt,
93
+ minutesRemaining: tokenStatus.minutesRemaining,
94
+ },
95
+ messaging: {
96
+ available: messageAuth !== null,
97
+ },
98
+ favorites: {
99
+ available: messageAuth !== null && csaToken !== null,
100
+ },
101
+ session: {
102
+ exists: sessionExists,
103
+ likelyExpired: sessionExpired,
104
+ },
105
+ browser: {
106
+ running: browserManager !== null,
107
+ initialised: ctx.server.isInitialisedState(),
108
+ },
109
+ authentication: authStatus,
110
+ },
111
+ };
112
+ }
113
+ // ─────────────────────────────────────────────────────────────────────────────
114
+ // Exports
115
+ // ─────────────────────────────────────────────────────────────────────────────
116
+ export const loginTool = {
117
+ definition: loginToolDefinition,
118
+ schema: LoginInputSchema,
119
+ handler: handleLogin,
120
+ };
121
+ export const statusTool = {
122
+ definition: statusToolDefinition,
123
+ schema: z.object({}),
124
+ handler: handleStatus,
125
+ };
126
+ /** All auth-related tools. */
127
+ export const authTools = [loginTool, statusTool];
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Tool handler registry.
3
+ *
4
+ * Provides a modular way to define MCP tool handlers without a monolithic
5
+ * switch statement. Each tool is defined with its schema, handler, and metadata.
6
+ */
7
+ import type { Tool } from '@modelcontextprotocol/sdk/types.js';
8
+ import type { z } from 'zod';
9
+ import type { McpError } from '../types/errors.js';
10
+ import type { BrowserManager } from '../browser/context.js';
11
+ export interface TeamsServer {
12
+ ensureBrowser(headless?: boolean): Promise<BrowserManager>;
13
+ resetBrowserState(): void;
14
+ getBrowserManager(): BrowserManager | null;
15
+ setBrowserManager(manager: BrowserManager): void;
16
+ markInitialised(): void;
17
+ isInitialisedState(): boolean;
18
+ }
19
+ /** The context passed to tool handlers. */
20
+ export interface ToolContext {
21
+ /** Reference to the server for browser operations. */
22
+ server: TeamsServer;
23
+ }
24
+ /** Result returned by tool handlers. */
25
+ export type ToolResult = {
26
+ success: true;
27
+ data: Record<string, unknown>;
28
+ } | {
29
+ success: false;
30
+ error: McpError;
31
+ };
32
+ /** A registered tool with its handler. */
33
+ export interface RegisteredTool<TInput extends z.ZodType = z.ZodType> {
34
+ /** Tool definition for MCP. */
35
+ definition: Tool;
36
+ /** Zod schema for input validation. */
37
+ schema: TInput;
38
+ /** Handler function. */
39
+ handler: (input: z.infer<TInput>, ctx: ToolContext) => Promise<ToolResult>;
40
+ }
41
+ export * from './search-tools.js';
42
+ export * from './message-tools.js';
43
+ export * from './people-tools.js';
44
+ export * from './auth-tools.js';
45
+ export * from './registry.js';
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Tool handler registry.
3
+ *
4
+ * Provides a modular way to define MCP tool handlers without a monolithic
5
+ * switch statement. Each tool is defined with its schema, handler, and metadata.
6
+ */
7
+ // Re-export tool registrations
8
+ export * from './search-tools.js';
9
+ export * from './message-tools.js';
10
+ export * from './people-tools.js';
11
+ export * from './auth-tools.js';
12
+ export * from './registry.js';