msteams-mcp 0.2.1

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.

Potentially problematic release.


This version of msteams-mcp might be problematic. Click here for more details.

Files changed (77) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +261 -0
  3. package/dist/__fixtures__/api-responses.d.ts +254 -0
  4. package/dist/__fixtures__/api-responses.js +245 -0
  5. package/dist/api/calendar-api.d.ts +66 -0
  6. package/dist/api/calendar-api.js +179 -0
  7. package/dist/api/chatsvc-api.d.ts +352 -0
  8. package/dist/api/chatsvc-api.js +1100 -0
  9. package/dist/api/csa-api.d.ts +64 -0
  10. package/dist/api/csa-api.js +200 -0
  11. package/dist/api/index.d.ts +7 -0
  12. package/dist/api/index.js +7 -0
  13. package/dist/api/substrate-api.d.ts +50 -0
  14. package/dist/api/substrate-api.js +305 -0
  15. package/dist/auth/crypto.d.ts +32 -0
  16. package/dist/auth/crypto.js +66 -0
  17. package/dist/auth/index.d.ts +7 -0
  18. package/dist/auth/index.js +7 -0
  19. package/dist/auth/session-store.d.ts +87 -0
  20. package/dist/auth/session-store.js +230 -0
  21. package/dist/auth/token-extractor.d.ts +185 -0
  22. package/dist/auth/token-extractor.js +674 -0
  23. package/dist/auth/token-refresh.d.ts +25 -0
  24. package/dist/auth/token-refresh.js +85 -0
  25. package/dist/browser/auth.d.ts +53 -0
  26. package/dist/browser/auth.js +603 -0
  27. package/dist/browser/context.d.ts +40 -0
  28. package/dist/browser/context.js +122 -0
  29. package/dist/constants.d.ts +104 -0
  30. package/dist/constants.js +195 -0
  31. package/dist/index.d.ts +8 -0
  32. package/dist/index.js +12 -0
  33. package/dist/research/auth-research.d.ts +10 -0
  34. package/dist/research/auth-research.js +175 -0
  35. package/dist/research/explore.d.ts +11 -0
  36. package/dist/research/explore.js +270 -0
  37. package/dist/research/search-research.d.ts +17 -0
  38. package/dist/research/search-research.js +317 -0
  39. package/dist/server.d.ts +66 -0
  40. package/dist/server.js +295 -0
  41. package/dist/test/debug-search.d.ts +10 -0
  42. package/dist/test/debug-search.js +147 -0
  43. package/dist/test/mcp-harness.d.ts +17 -0
  44. package/dist/test/mcp-harness.js +474 -0
  45. package/dist/tools/auth-tools.d.ts +26 -0
  46. package/dist/tools/auth-tools.js +191 -0
  47. package/dist/tools/index.d.ts +56 -0
  48. package/dist/tools/index.js +34 -0
  49. package/dist/tools/meeting-tools.d.ts +33 -0
  50. package/dist/tools/meeting-tools.js +64 -0
  51. package/dist/tools/message-tools.d.ts +269 -0
  52. package/dist/tools/message-tools.js +856 -0
  53. package/dist/tools/people-tools.d.ts +46 -0
  54. package/dist/tools/people-tools.js +112 -0
  55. package/dist/tools/registry.d.ts +23 -0
  56. package/dist/tools/registry.js +63 -0
  57. package/dist/tools/search-tools.d.ts +91 -0
  58. package/dist/tools/search-tools.js +222 -0
  59. package/dist/types/errors.d.ts +58 -0
  60. package/dist/types/errors.js +132 -0
  61. package/dist/types/result.d.ts +43 -0
  62. package/dist/types/result.js +51 -0
  63. package/dist/types/server.d.ts +27 -0
  64. package/dist/types/server.js +7 -0
  65. package/dist/types/teams.d.ts +85 -0
  66. package/dist/types/teams.js +4 -0
  67. package/dist/utils/api-config.d.ts +103 -0
  68. package/dist/utils/api-config.js +158 -0
  69. package/dist/utils/auth-guards.d.ts +67 -0
  70. package/dist/utils/auth-guards.js +147 -0
  71. package/dist/utils/http.d.ts +29 -0
  72. package/dist/utils/http.js +112 -0
  73. package/dist/utils/parsers.d.ts +247 -0
  74. package/dist/utils/parsers.js +731 -0
  75. package/dist/utils/parsers.test.d.ts +7 -0
  76. package/dist/utils/parsers.test.js +511 -0
  77. package/package.json +62 -0
package/dist/server.js ADDED
@@ -0,0 +1,295 @@
1
+ /**
2
+ * MCP Server implementation for Teams search.
3
+ * Exposes tools and resources for interacting with Microsoft Teams.
4
+ */
5
+ import { createRequire } from 'module';
6
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
7
+ const require = createRequire(import.meta.url);
8
+ const pkg = require('../package.json');
9
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
10
+ import { CallToolRequestSchema, ListToolsRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
11
+ import { createBrowserContext, closeBrowser } from './browser/context.js';
12
+ import { ensureAuthenticated } from './browser/auth.js';
13
+ // Auth modules
14
+ import { hasSessionState, isSessionLikelyExpired, } from './auth/session-store.js';
15
+ import { getSubstrateTokenStatus, extractMessageAuth, extractCsaToken, getUserProfile, } from './auth/token-extractor.js';
16
+ // API modules
17
+ import { getFavorites } from './api/csa-api.js';
18
+ // Tool registry
19
+ import { getToolDefinitions, invokeTool } from './tools/registry.js';
20
+ // Types
21
+ import { ErrorCode, createError } from './types/errors.js';
22
+ // ─────────────────────────────────────────────────────────────────────────────
23
+ // MCP Server Class
24
+ // ─────────────────────────────────────────────────────────────────────────────
25
+ /**
26
+ * MCP Server for Teams integration.
27
+ *
28
+ * Encapsulates all server state to allow multiple instances.
29
+ * Implements ITeamsServer interface for use by tool handlers.
30
+ */
31
+ export class TeamsServer {
32
+ browserManager = null;
33
+ isInitialised = false;
34
+ // ───────────────────────────────────────────────────────────────────────────
35
+ // Response Formatting
36
+ // ───────────────────────────────────────────────────────────────────────────
37
+ /**
38
+ * Returns a standard MCP error response.
39
+ */
40
+ formatError(error) {
41
+ return {
42
+ content: [
43
+ {
44
+ type: 'text',
45
+ text: JSON.stringify({
46
+ success: false,
47
+ error: error.message,
48
+ errorCode: error.code,
49
+ retryable: error.retryable,
50
+ retryAfterMs: error.retryAfterMs,
51
+ suggestions: error.suggestions,
52
+ }, null, 2),
53
+ },
54
+ ],
55
+ isError: true,
56
+ };
57
+ }
58
+ /**
59
+ * Returns a standard MCP success response.
60
+ */
61
+ formatSuccess(data) {
62
+ return {
63
+ content: [
64
+ {
65
+ type: 'text',
66
+ text: JSON.stringify({ success: true, ...data }, null, 2),
67
+ },
68
+ ],
69
+ };
70
+ }
71
+ // ───────────────────────────────────────────────────────────────────────────
72
+ // Browser State Management (exposed for tool handlers)
73
+ // ───────────────────────────────────────────────────────────────────────────
74
+ /**
75
+ * Gets the current browser manager.
76
+ */
77
+ getBrowserManager() {
78
+ return this.browserManager;
79
+ }
80
+ /**
81
+ * Sets the browser manager.
82
+ */
83
+ setBrowserManager(manager) {
84
+ this.browserManager = manager;
85
+ }
86
+ /**
87
+ * Resets browser state.
88
+ */
89
+ resetBrowserState() {
90
+ this.browserManager = null;
91
+ this.isInitialised = false;
92
+ }
93
+ /**
94
+ * Marks the server as initialised.
95
+ */
96
+ markInitialised() {
97
+ this.isInitialised = true;
98
+ }
99
+ /**
100
+ * Checks if the server is initialised.
101
+ */
102
+ isInitialisedState() {
103
+ return this.isInitialised;
104
+ }
105
+ /**
106
+ * Ensures the browser is running and authenticated.
107
+ */
108
+ async ensureBrowser(headless = true) {
109
+ if (this.browserManager && this.isInitialised) {
110
+ return this.browserManager;
111
+ }
112
+ // Close existing browser if any
113
+ if (this.browserManager) {
114
+ try {
115
+ await closeBrowser(this.browserManager, true);
116
+ }
117
+ catch {
118
+ // Ignore cleanup errors
119
+ }
120
+ }
121
+ this.browserManager = await createBrowserContext({ headless });
122
+ await ensureAuthenticated(this.browserManager.page, this.browserManager.context, (msg) => console.error(`[auth] ${msg}`));
123
+ this.isInitialised = true;
124
+ return this.browserManager;
125
+ }
126
+ /**
127
+ * Cleans up browser resources.
128
+ */
129
+ async cleanup() {
130
+ if (this.browserManager) {
131
+ await closeBrowser(this.browserManager, true);
132
+ this.browserManager = null;
133
+ this.isInitialised = false;
134
+ }
135
+ }
136
+ // ───────────────────────────────────────────────────────────────────────────
137
+ // Server Creation
138
+ // ───────────────────────────────────────────────────────────────────────────
139
+ /**
140
+ * Creates and configures the MCP server.
141
+ */
142
+ async createServer() {
143
+ const server = new Server({
144
+ name: 'teams-mcp',
145
+ version: pkg.version,
146
+ }, {
147
+ capabilities: {
148
+ tools: {},
149
+ resources: {},
150
+ },
151
+ });
152
+ // Handle resource listing
153
+ server.setRequestHandler(ListResourcesRequestSchema, async () => {
154
+ return {
155
+ resources: [
156
+ {
157
+ uri: 'teams://me/profile',
158
+ name: 'Current User Profile',
159
+ description: 'The authenticated user\'s Teams profile including email and display name',
160
+ mimeType: 'application/json',
161
+ },
162
+ {
163
+ uri: 'teams://me/favorites',
164
+ name: 'Pinned Conversations',
165
+ description: 'The user\'s favourite/pinned Teams conversations',
166
+ mimeType: 'application/json',
167
+ },
168
+ {
169
+ uri: 'teams://status',
170
+ name: 'Authentication Status',
171
+ description: 'Current authentication status for all Teams APIs',
172
+ mimeType: 'application/json',
173
+ },
174
+ ],
175
+ };
176
+ });
177
+ // Handle resource reading
178
+ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
179
+ const { uri } = request.params;
180
+ switch (uri) {
181
+ case 'teams://me/profile': {
182
+ const profile = getUserProfile();
183
+ return {
184
+ contents: [
185
+ {
186
+ uri,
187
+ mimeType: 'application/json',
188
+ text: JSON.stringify(profile ?? { error: 'No valid session' }, null, 2),
189
+ },
190
+ ],
191
+ };
192
+ }
193
+ case 'teams://me/favorites': {
194
+ const result = await getFavorites();
195
+ return {
196
+ contents: [
197
+ {
198
+ uri,
199
+ mimeType: 'application/json',
200
+ text: JSON.stringify(result.ok ? result.value.favorites : { error: result.error.message }, null, 2),
201
+ },
202
+ ],
203
+ };
204
+ }
205
+ case 'teams://status': {
206
+ const tokenStatus = getSubstrateTokenStatus();
207
+ const messageAuth = extractMessageAuth();
208
+ const csaToken = extractCsaToken();
209
+ const status = {
210
+ directApi: {
211
+ available: tokenStatus.hasToken,
212
+ expiresAt: tokenStatus.expiresAt,
213
+ minutesRemaining: tokenStatus.minutesRemaining,
214
+ },
215
+ messaging: {
216
+ available: messageAuth !== null,
217
+ },
218
+ favorites: {
219
+ available: messageAuth !== null && csaToken !== null,
220
+ },
221
+ session: {
222
+ exists: hasSessionState(),
223
+ likelyExpired: isSessionLikelyExpired(),
224
+ },
225
+ };
226
+ return {
227
+ contents: [
228
+ {
229
+ uri,
230
+ mimeType: 'application/json',
231
+ text: JSON.stringify(status, null, 2),
232
+ },
233
+ ],
234
+ };
235
+ }
236
+ default:
237
+ throw new Error(`Unknown resource: ${uri}`);
238
+ }
239
+ });
240
+ // Handle tool listing
241
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
242
+ return { tools: getToolDefinitions() };
243
+ });
244
+ // Handle tool calls
245
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
246
+ const { name, arguments: args } = request.params;
247
+ try {
248
+ const ctx = { server: this };
249
+ const result = await invokeTool(name, args, ctx);
250
+ if (result.success) {
251
+ return this.formatSuccess(result.data);
252
+ }
253
+ else {
254
+ return this.formatError(result.error);
255
+ }
256
+ }
257
+ catch (error) {
258
+ const message = error instanceof Error ? error.message : String(error);
259
+ return this.formatError(createError(ErrorCode.UNKNOWN, message, { retryable: false }));
260
+ }
261
+ });
262
+ // Cleanup on server close
263
+ server.onclose = async () => {
264
+ await this.cleanup();
265
+ };
266
+ return server;
267
+ }
268
+ }
269
+ // ─────────────────────────────────────────────────────────────────────────────
270
+ // Exports
271
+ // ─────────────────────────────────────────────────────────────────────────────
272
+ /**
273
+ * Creates and runs the MCP server.
274
+ * Exported for backward compatibility.
275
+ */
276
+ export async function createServer() {
277
+ const teamsServer = new TeamsServer();
278
+ return teamsServer.createServer();
279
+ }
280
+ /**
281
+ * Runs the server with stdio transport.
282
+ */
283
+ export async function runServer() {
284
+ const teamsServer = new TeamsServer();
285
+ const server = await teamsServer.createServer();
286
+ const transport = new StdioServerTransport();
287
+ await server.connect(transport);
288
+ // Handle shutdown signals
289
+ const shutdown = async () => {
290
+ await teamsServer.cleanup();
291
+ process.exit(0);
292
+ };
293
+ process.on('SIGINT', shutdown);
294
+ process.on('SIGTERM', shutdown);
295
+ }
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env npx tsx
2
+ /**
3
+ * Debug script to inspect the Teams search page.
4
+ * Takes screenshots and dumps page structure for debugging selectors.
5
+ *
6
+ * Usage:
7
+ * npm run debug:search
8
+ * npm run debug:search -- "search query"
9
+ */
10
+ export {};
@@ -0,0 +1,147 @@
1
+ #!/usr/bin/env npx tsx
2
+ /**
3
+ * Debug script to inspect the Teams search page.
4
+ * Takes screenshots and dumps page structure for debugging selectors.
5
+ *
6
+ * Usage:
7
+ * npm run debug:search
8
+ * npm run debug:search -- "search query"
9
+ */
10
+ import { createBrowserContext, closeBrowser } from '../browser/context.js';
11
+ import { ensureAuthenticated } from '../browser/auth.js';
12
+ import * as fs from 'fs';
13
+ import * as path from 'path';
14
+ import { fileURLToPath } from 'url';
15
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
16
+ const PROJECT_ROOT = path.resolve(__dirname, '../..');
17
+ const DEBUG_DIR = path.join(PROJECT_ROOT, 'debug-output');
18
+ async function main() {
19
+ const query = process.argv[2] ?? 'test';
20
+ console.log('šŸ” Debug Search Script\n');
21
+ console.log(`Query: "${query}"`);
22
+ // Ensure debug output directory exists
23
+ if (!fs.existsSync(DEBUG_DIR)) {
24
+ fs.mkdirSync(DEBUG_DIR, { recursive: true });
25
+ }
26
+ const manager = await createBrowserContext({ headless: false });
27
+ try {
28
+ // Authenticate
29
+ console.log('\n1. Authenticating...');
30
+ await ensureAuthenticated(manager.page, manager.context);
31
+ // Take initial screenshot
32
+ console.log('\n2. Taking initial screenshot...');
33
+ await manager.page.screenshot({
34
+ path: path.join(DEBUG_DIR, '01-initial.png'),
35
+ fullPage: true
36
+ });
37
+ // Wait for page to stabilise
38
+ await manager.page.waitForTimeout(3000);
39
+ // Open search with keyboard shortcut
40
+ console.log('\n3. Opening search (Cmd+E)...');
41
+ const isMac = process.platform === 'darwin';
42
+ await manager.page.keyboard.press(isMac ? 'Meta+e' : 'Control+e');
43
+ await manager.page.waitForTimeout(2000);
44
+ await manager.page.screenshot({
45
+ path: path.join(DEBUG_DIR, '02-search-opened.png'),
46
+ fullPage: true
47
+ });
48
+ // Find and list all inputs
49
+ console.log('\n4. Scanning for input elements...');
50
+ const inputs = await manager.page.locator('input').all();
51
+ console.log(` Found ${inputs.length} input elements:`);
52
+ for (let i = 0; i < inputs.length; i++) {
53
+ const input = inputs[i];
54
+ try {
55
+ const isVisible = await input.isVisible();
56
+ if (!isVisible)
57
+ continue;
58
+ const placeholder = await input.getAttribute('placeholder') ?? '';
59
+ const ariaLabel = await input.getAttribute('aria-label') ?? '';
60
+ const dataTid = await input.getAttribute('data-tid') ?? '';
61
+ const type = await input.getAttribute('type') ?? '';
62
+ const id = await input.getAttribute('id') ?? '';
63
+ console.log(` [${i}] visible=true, placeholder="${placeholder}", aria-label="${ariaLabel}", data-tid="${dataTid}", type="${type}", id="${id}"`);
64
+ }
65
+ catch {
66
+ // Skip
67
+ }
68
+ }
69
+ // Try to find and use search input
70
+ console.log('\n5. Looking for search input...');
71
+ const searchSelectors = [
72
+ 'input[placeholder*="Search" i]',
73
+ 'input[aria-label*="Search" i]',
74
+ '[data-tid*="search"] input',
75
+ 'input[type="search"]',
76
+ ];
77
+ let searchInput = null;
78
+ for (const sel of searchSelectors) {
79
+ const loc = manager.page.locator(sel).first();
80
+ if (await loc.count() > 0 && await loc.isVisible()) {
81
+ console.log(` Found with selector: ${sel}`);
82
+ searchInput = loc;
83
+ break;
84
+ }
85
+ }
86
+ if (searchInput) {
87
+ // Type the query
88
+ console.log('\n6. Typing query...');
89
+ await searchInput.fill(query);
90
+ await manager.page.waitForTimeout(500);
91
+ await manager.page.screenshot({
92
+ path: path.join(DEBUG_DIR, '03-query-typed.png'),
93
+ fullPage: true
94
+ });
95
+ // Submit
96
+ console.log('\n7. Submitting search (Enter)...');
97
+ await manager.page.keyboard.press('Enter');
98
+ await manager.page.waitForTimeout(5000);
99
+ await manager.page.screenshot({
100
+ path: path.join(DEBUG_DIR, '04-results.png'),
101
+ fullPage: true
102
+ });
103
+ // Scan for result elements
104
+ console.log('\n8. Scanning for result elements...');
105
+ const resultSelectors = [
106
+ '[data-tid*="search"]',
107
+ '[data-tid*="result"]',
108
+ '[role="listitem"]',
109
+ '[role="option"]',
110
+ '.search-result',
111
+ '[data-tid*="message"]',
112
+ ];
113
+ for (const sel of resultSelectors) {
114
+ const count = await manager.page.locator(sel).count();
115
+ if (count > 0) {
116
+ console.log(` ${sel}: ${count} elements`);
117
+ // Get first element's text content preview
118
+ const first = manager.page.locator(sel).first();
119
+ const text = await first.textContent().catch(() => null);
120
+ if (text) {
121
+ console.log(` Preview: "${text.substring(0, 80).replace(/\n/g, ' ')}..."`);
122
+ }
123
+ }
124
+ }
125
+ // Dump page HTML structure (simplified)
126
+ console.log('\n9. Dumping main content structure...');
127
+ const mainContent = await manager.page.locator('main, [role="main"], #app, .app-container').first();
128
+ if (await mainContent.count() > 0) {
129
+ const html = await mainContent.innerHTML();
130
+ fs.writeFileSync(path.join(DEBUG_DIR, 'page-structure.html'), html.substring(0, 50000) // First 50KB
131
+ );
132
+ console.log(' Saved to debug-output/page-structure.html');
133
+ }
134
+ }
135
+ else {
136
+ console.log(' āŒ Could not find search input');
137
+ }
138
+ console.log('\n10. Keeping browser open for 30 seconds for manual inspection...');
139
+ console.log(` Screenshots saved to: ${DEBUG_DIR}`);
140
+ await manager.page.waitForTimeout(30000);
141
+ }
142
+ finally {
143
+ await closeBrowser(manager, true);
144
+ }
145
+ console.log('\nāœ… Debug session complete');
146
+ }
147
+ main().catch(console.error);
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env npx tsx
2
+ /**
3
+ * MCP Protocol Test Harness
4
+ *
5
+ * Tests the MCP server by connecting a client through the actual MCP protocol,
6
+ * rather than calling underlying functions directly. This ensures the full
7
+ * protocol layer works correctly.
8
+ *
9
+ * Usage:
10
+ * npm run test:mcp # List tools and check status
11
+ * npm run test:mcp -- search "query" # Search for messages (shortcut)
12
+ * npm run test:mcp -- teams_search --query "q" # Generic tool call
13
+ * npm run test:mcp -- --json # Output as JSON
14
+ *
15
+ * Any unrecognised command is treated as a tool name. Use --key value for parameters.
16
+ */
17
+ export {};