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.
- package/LICENSE +21 -0
- package/README.md +261 -0
- package/dist/__fixtures__/api-responses.d.ts +254 -0
- package/dist/__fixtures__/api-responses.js +245 -0
- package/dist/api/calendar-api.d.ts +66 -0
- package/dist/api/calendar-api.js +179 -0
- package/dist/api/chatsvc-api.d.ts +352 -0
- package/dist/api/chatsvc-api.js +1100 -0
- package/dist/api/csa-api.d.ts +64 -0
- package/dist/api/csa-api.js +200 -0
- package/dist/api/index.d.ts +7 -0
- package/dist/api/index.js +7 -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 +7 -0
- package/dist/auth/index.js +7 -0
- package/dist/auth/session-store.d.ts +87 -0
- package/dist/auth/session-store.js +230 -0
- package/dist/auth/token-extractor.d.ts +185 -0
- package/dist/auth/token-extractor.js +674 -0
- package/dist/auth/token-refresh.d.ts +25 -0
- package/dist/auth/token-refresh.js +85 -0
- package/dist/browser/auth.d.ts +53 -0
- package/dist/browser/auth.js +603 -0
- package/dist/browser/context.d.ts +40 -0
- package/dist/browser/context.js +122 -0
- package/dist/constants.d.ts +104 -0
- package/dist/constants.js +195 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +12 -0
- package/dist/research/auth-research.d.ts +10 -0
- package/dist/research/auth-research.js +175 -0
- package/dist/research/explore.d.ts +11 -0
- package/dist/research/explore.js +270 -0
- package/dist/research/search-research.d.ts +17 -0
- package/dist/research/search-research.js +317 -0
- package/dist/server.d.ts +66 -0
- package/dist/server.js +295 -0
- package/dist/test/debug-search.d.ts +10 -0
- package/dist/test/debug-search.js +147 -0
- package/dist/test/mcp-harness.d.ts +17 -0
- package/dist/test/mcp-harness.js +474 -0
- package/dist/tools/auth-tools.d.ts +26 -0
- package/dist/tools/auth-tools.js +191 -0
- package/dist/tools/index.d.ts +56 -0
- package/dist/tools/index.js +34 -0
- package/dist/tools/meeting-tools.d.ts +33 -0
- package/dist/tools/meeting-tools.js +64 -0
- package/dist/tools/message-tools.d.ts +269 -0
- package/dist/tools/message-tools.js +856 -0
- package/dist/tools/people-tools.d.ts +46 -0
- package/dist/tools/people-tools.js +112 -0
- package/dist/tools/registry.d.ts +23 -0
- package/dist/tools/registry.js +63 -0
- package/dist/tools/search-tools.d.ts +91 -0
- package/dist/tools/search-tools.js +222 -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/server.d.ts +27 -0
- package/dist/types/server.js +7 -0
- package/dist/types/teams.d.ts +85 -0
- package/dist/types/teams.js +4 -0
- package/dist/utils/api-config.d.ts +103 -0
- package/dist/utils/api-config.js +158 -0
- package/dist/utils/auth-guards.d.ts +67 -0
- package/dist/utils/auth-guards.js +147 -0
- package/dist/utils/http.d.ts +29 -0
- package/dist/utils/http.js +112 -0
- package/dist/utils/parsers.d.ts +247 -0
- package/dist/utils/parsers.js +731 -0
- package/dist/utils/parsers.test.d.ts +7 -0
- package/dist/utils/parsers.test.js +511 -0
- package/package.json +62 -0
|
@@ -0,0 +1,474 @@
|
|
|
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
|
+
me: { tool: 'teams_get_me' },
|
|
26
|
+
people: { tool: 'teams_search_people', primaryArg: 'query' },
|
|
27
|
+
favorites: { tool: 'teams_get_favorites' },
|
|
28
|
+
save: { tool: 'teams_save_message' },
|
|
29
|
+
unsave: { tool: 'teams_unsave_message' },
|
|
30
|
+
thread: { tool: 'teams_get_thread' },
|
|
31
|
+
login: { tool: 'teams_login' },
|
|
32
|
+
contacts: { tool: 'teams_get_frequent_contacts' },
|
|
33
|
+
channel: { tool: 'teams_find_channel', primaryArg: 'query' },
|
|
34
|
+
chat: { tool: 'teams_get_chat', primaryArg: 'userId' },
|
|
35
|
+
groupchat: { tool: 'teams_create_group_chat' },
|
|
36
|
+
unread: { tool: 'teams_get_unread' },
|
|
37
|
+
markread: { tool: 'teams_mark_read' },
|
|
38
|
+
activity: { tool: 'teams_get_activity' },
|
|
39
|
+
};
|
|
40
|
+
// Map CLI flags to tool parameter names
|
|
41
|
+
const FLAG_MAPPINGS = {
|
|
42
|
+
'--to': 'conversationId',
|
|
43
|
+
'--message': 'messageId',
|
|
44
|
+
'--reply': 'replyToMessageId',
|
|
45
|
+
'--replyTo': 'replyToMessageId',
|
|
46
|
+
'--from': 'from',
|
|
47
|
+
'--size': 'size',
|
|
48
|
+
'--limit': 'limit',
|
|
49
|
+
'--query': 'query',
|
|
50
|
+
'--content': 'content',
|
|
51
|
+
'--force': 'forceNew',
|
|
52
|
+
'--user': 'userId',
|
|
53
|
+
'--userId': 'userId',
|
|
54
|
+
'--userIds': 'userIds',
|
|
55
|
+
'--topic': 'topic',
|
|
56
|
+
'--markRead': 'markRead',
|
|
57
|
+
};
|
|
58
|
+
function parseArgs() {
|
|
59
|
+
const args = process.argv.slice(2);
|
|
60
|
+
const result = {
|
|
61
|
+
command: 'list',
|
|
62
|
+
toolName: null,
|
|
63
|
+
primaryArg: null,
|
|
64
|
+
args: {},
|
|
65
|
+
json: false,
|
|
66
|
+
};
|
|
67
|
+
let i = 0;
|
|
68
|
+
// First pass: find the command (first non-flag argument)
|
|
69
|
+
while (i < args.length) {
|
|
70
|
+
const arg = args[i];
|
|
71
|
+
if (arg === '--json') {
|
|
72
|
+
result.json = true;
|
|
73
|
+
i++;
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (arg.startsWith('--')) {
|
|
77
|
+
// Skip flag and its value
|
|
78
|
+
i += 2;
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
// Found the command
|
|
82
|
+
result.command = arg;
|
|
83
|
+
i++;
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
// Check if it's a shortcut
|
|
87
|
+
const shortcut = SHORTCUTS[result.command];
|
|
88
|
+
if (shortcut) {
|
|
89
|
+
result.toolName = shortcut.tool;
|
|
90
|
+
// Look for primary argument (next non-flag arg)
|
|
91
|
+
while (i < args.length) {
|
|
92
|
+
const arg = args[i];
|
|
93
|
+
if (!arg.startsWith('--')) {
|
|
94
|
+
if (shortcut.primaryArg) {
|
|
95
|
+
result.args[shortcut.primaryArg] = arg;
|
|
96
|
+
}
|
|
97
|
+
result.primaryArg = arg;
|
|
98
|
+
i++;
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
i++;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
else if (result.command !== 'list') {
|
|
105
|
+
// Treat as a tool name (add teams_ prefix if not present)
|
|
106
|
+
result.toolName = result.command.startsWith('teams_')
|
|
107
|
+
? result.command
|
|
108
|
+
: `teams_${result.command}`;
|
|
109
|
+
}
|
|
110
|
+
// Second pass: collect all --key value pairs
|
|
111
|
+
for (let j = 0; j < args.length; j++) {
|
|
112
|
+
const arg = args[j];
|
|
113
|
+
if (arg === '--json') {
|
|
114
|
+
result.json = true;
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
if (arg.startsWith('--') && args[j + 1] !== undefined) {
|
|
118
|
+
const key = FLAG_MAPPINGS[arg] || arg.slice(2); // Use mapping or strip --
|
|
119
|
+
let value = args[j + 1];
|
|
120
|
+
// Only parse booleans and specific numeric fields
|
|
121
|
+
// Don't coerce messageId, conversationId etc. - they're strings
|
|
122
|
+
const numericFields = new Set(['from', 'size', 'limit']);
|
|
123
|
+
if (value === 'true')
|
|
124
|
+
value = true;
|
|
125
|
+
else if (value === 'false')
|
|
126
|
+
value = false;
|
|
127
|
+
else if (numericFields.has(key) && /^\d+$/.test(value)) {
|
|
128
|
+
value = parseInt(value, 10);
|
|
129
|
+
}
|
|
130
|
+
else if (typeof value === 'string' && (value.startsWith('[') || value.startsWith('{'))) {
|
|
131
|
+
// Try to parse JSON for array or object values
|
|
132
|
+
try {
|
|
133
|
+
value = JSON.parse(value);
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
// Keep as string if not valid JSON
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
result.args[key] = value;
|
|
140
|
+
j++; // Skip the value
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return result;
|
|
144
|
+
}
|
|
145
|
+
function log(message) {
|
|
146
|
+
console.log(message);
|
|
147
|
+
}
|
|
148
|
+
function logSection(title) {
|
|
149
|
+
console.log('\n' + '─'.repeat(50));
|
|
150
|
+
console.log(` ${title}`);
|
|
151
|
+
console.log('─'.repeat(50));
|
|
152
|
+
}
|
|
153
|
+
async function createTestClient() {
|
|
154
|
+
// Create the MCP server
|
|
155
|
+
const server = await createServer();
|
|
156
|
+
// Create linked in-memory transports
|
|
157
|
+
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
|
|
158
|
+
// Connect the server to its transport
|
|
159
|
+
await server.connect(serverTransport);
|
|
160
|
+
// Create and connect the client
|
|
161
|
+
const client = new Client({ name: 'mcp-test-harness', version: '1.0.0' }, { capabilities: {} });
|
|
162
|
+
await client.connect(clientTransport);
|
|
163
|
+
const cleanup = async () => {
|
|
164
|
+
await client.close();
|
|
165
|
+
await server.close();
|
|
166
|
+
};
|
|
167
|
+
return { client, cleanup };
|
|
168
|
+
}
|
|
169
|
+
async function listTools(client, json) {
|
|
170
|
+
if (!json) {
|
|
171
|
+
logSection('Available Tools');
|
|
172
|
+
}
|
|
173
|
+
const result = await client.listTools();
|
|
174
|
+
if (json) {
|
|
175
|
+
console.log(JSON.stringify(result, null, 2));
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
log(`Found ${result.tools.length} tools:\n`);
|
|
179
|
+
for (const tool of result.tools) {
|
|
180
|
+
log(`📦 ${tool.name}`);
|
|
181
|
+
log(` ${tool.description}`);
|
|
182
|
+
const schema = tool.inputSchema;
|
|
183
|
+
if (schema.properties) {
|
|
184
|
+
const props = Object.entries(schema.properties);
|
|
185
|
+
const required = new Set(schema.required ?? []);
|
|
186
|
+
for (const [name, prop] of props) {
|
|
187
|
+
const propObj = prop;
|
|
188
|
+
const reqMark = required.has(name) ? ' (required)' : '';
|
|
189
|
+
log(` - ${name}: ${propObj.type ?? 'any'}${reqMark}`);
|
|
190
|
+
if (propObj.description) {
|
|
191
|
+
log(` ${propObj.description}`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
log('');
|
|
196
|
+
}
|
|
197
|
+
// Show available shortcuts
|
|
198
|
+
log('Shortcuts:');
|
|
199
|
+
for (const [shortcut, config] of Object.entries(SHORTCUTS)) {
|
|
200
|
+
const primaryNote = config.primaryArg ? ` <${config.primaryArg}>` : '';
|
|
201
|
+
log(` ${shortcut}${primaryNote} → ${config.tool}`);
|
|
202
|
+
}
|
|
203
|
+
log('');
|
|
204
|
+
log('Any unrecognised command is treated as a tool name.');
|
|
205
|
+
log('Use --key value for parameters, e.g.: teams_find_channel --query "name"');
|
|
206
|
+
}
|
|
207
|
+
async function callTool(client, toolName, args, json) {
|
|
208
|
+
// Verify the tool exists
|
|
209
|
+
const tools = await client.listTools();
|
|
210
|
+
const tool = tools.tools.find(t => t.name === toolName);
|
|
211
|
+
if (!tool) {
|
|
212
|
+
console.error(`❌ Unknown tool: ${toolName}`);
|
|
213
|
+
console.error('');
|
|
214
|
+
console.error('Available tools:');
|
|
215
|
+
for (const t of tools.tools) {
|
|
216
|
+
console.error(` - ${t.name}`);
|
|
217
|
+
}
|
|
218
|
+
process.exit(1);
|
|
219
|
+
}
|
|
220
|
+
if (!json) {
|
|
221
|
+
logSection(`Calling: ${toolName}`);
|
|
222
|
+
if (Object.keys(args).length > 0) {
|
|
223
|
+
log(`Arguments: ${JSON.stringify(args)}\n`);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
// Login tool needs a longer timeout for MFA flows
|
|
227
|
+
const timeout = toolName === 'teams_login' ? 5 * 60 * 1000 : undefined; // 5 minutes for login
|
|
228
|
+
const result = await client.callTool({ name: toolName, arguments: args }, undefined, { timeout });
|
|
229
|
+
if (json) {
|
|
230
|
+
console.log(JSON.stringify(result, null, 2));
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
// Pretty-print the result
|
|
234
|
+
const content = result.content;
|
|
235
|
+
const textContent = content.find(c => c.type === 'text');
|
|
236
|
+
if (textContent?.text) {
|
|
237
|
+
try {
|
|
238
|
+
const response = JSON.parse(textContent.text);
|
|
239
|
+
prettyPrintResponse(response, toolName);
|
|
240
|
+
}
|
|
241
|
+
catch {
|
|
242
|
+
// Not JSON, just print as-is
|
|
243
|
+
log(textContent.text);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
else {
|
|
247
|
+
log('(No text content in response)');
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Pretty-prints a tool response based on common patterns
|
|
252
|
+
*/
|
|
253
|
+
function prettyPrintResponse(response, _toolName) {
|
|
254
|
+
// Check for error
|
|
255
|
+
if (response.success === false) {
|
|
256
|
+
log(`❌ Failed: ${response.error || 'Unknown error'}`);
|
|
257
|
+
if (response.code)
|
|
258
|
+
log(` Code: ${response.code}`);
|
|
259
|
+
if (response.suggestion)
|
|
260
|
+
log(` Suggestion: ${response.suggestion}`);
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
log('✅ Success\n');
|
|
264
|
+
// Handle common response shapes
|
|
265
|
+
if (response.results && Array.isArray(response.results)) {
|
|
266
|
+
printResultsList(response.results, response);
|
|
267
|
+
}
|
|
268
|
+
else if (response.favorites && Array.isArray(response.favorites)) {
|
|
269
|
+
printFavoritesList(response.favorites);
|
|
270
|
+
}
|
|
271
|
+
else if (response.messages && Array.isArray(response.messages)) {
|
|
272
|
+
printMessagesList(response.messages);
|
|
273
|
+
}
|
|
274
|
+
else if (response.activities && Array.isArray(response.activities)) {
|
|
275
|
+
printActivityList(response.activities);
|
|
276
|
+
}
|
|
277
|
+
else if (response.contacts && Array.isArray(response.contacts)) {
|
|
278
|
+
printContactsList(response.contacts);
|
|
279
|
+
}
|
|
280
|
+
else if (response.profile) {
|
|
281
|
+
printProfile(response.profile);
|
|
282
|
+
}
|
|
283
|
+
else {
|
|
284
|
+
// Generic output for other responses
|
|
285
|
+
printGenericResponse(response);
|
|
286
|
+
}
|
|
287
|
+
// Print pagination if present
|
|
288
|
+
if (response.pagination) {
|
|
289
|
+
const p = response.pagination;
|
|
290
|
+
log(`\nPagination: from=${p.from}, size=${p.size}, returned=${p.returned}`);
|
|
291
|
+
if (p.total !== undefined)
|
|
292
|
+
log(`Total available: ${p.total}`);
|
|
293
|
+
if (p.hasMore)
|
|
294
|
+
log(`More results available (use --from ${p.nextFrom})`);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
function printResultsList(results, response) {
|
|
298
|
+
log(`Found ${response.resultCount ?? results.length} results:\n`);
|
|
299
|
+
for (let i = 0; i < results.length; i++) {
|
|
300
|
+
const r = results[i];
|
|
301
|
+
const num = (response.pagination?.from ?? 0) + i + 1;
|
|
302
|
+
// Try to extract meaningful content
|
|
303
|
+
const content = String(r.content ?? r.displayName ?? r.name ?? '').substring(0, 100).replace(/\n/g, ' ');
|
|
304
|
+
log(`${num}. ${content}${content.length >= 100 ? '...' : ''}`);
|
|
305
|
+
// Print common fields
|
|
306
|
+
const sender = extractSenderName(r.sender);
|
|
307
|
+
if (sender)
|
|
308
|
+
log(` From: ${sender}`);
|
|
309
|
+
if (r.email)
|
|
310
|
+
log(` Email: ${r.email}`);
|
|
311
|
+
if (r.teamName)
|
|
312
|
+
log(` Team: ${r.teamName}`);
|
|
313
|
+
if (r.channelName && r.channelName !== r.teamName)
|
|
314
|
+
log(` Channel: ${r.channelName}`);
|
|
315
|
+
if (r.timestamp)
|
|
316
|
+
log(` Time: ${r.timestamp}`);
|
|
317
|
+
if (r.conversationId)
|
|
318
|
+
log(` ConversationId: ${r.conversationId}`);
|
|
319
|
+
if (r.jobTitle)
|
|
320
|
+
log(` Title: ${r.jobTitle}`);
|
|
321
|
+
if (r.department)
|
|
322
|
+
log(` Dept: ${r.department}`);
|
|
323
|
+
if (r.mri)
|
|
324
|
+
log(` MRI: ${r.mri}`);
|
|
325
|
+
log('');
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
function printFavoritesList(favorites) {
|
|
329
|
+
log(`Found ${favorites.length} favourites:\n`);
|
|
330
|
+
for (const f of favorites) {
|
|
331
|
+
const fav = f;
|
|
332
|
+
const typeLabel = fav.conversationType ? ` [${fav.conversationType}]` : '';
|
|
333
|
+
const nameLabel = fav.displayName || '(unnamed)';
|
|
334
|
+
log(`⭐ ${nameLabel}${typeLabel}`);
|
|
335
|
+
log(` ID: ${fav.conversationId}`);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
function printMessagesList(messages) {
|
|
339
|
+
log(`Got ${messages.length} messages:\n`);
|
|
340
|
+
for (const msg of messages) {
|
|
341
|
+
const m = msg;
|
|
342
|
+
const preview = String(m.content ?? '').substring(0, 100).replace(/\n/g, ' ');
|
|
343
|
+
const sender = m.sender?.displayName ||
|
|
344
|
+
m.sender?.mri || 'Unknown';
|
|
345
|
+
const time = m.timestamp ? new Date(m.timestamp).toLocaleString() : '';
|
|
346
|
+
const fromMe = m.isFromMe ? ' (you)' : '';
|
|
347
|
+
log(`📝 ${sender}${fromMe} - ${time}`);
|
|
348
|
+
log(` ${preview}${String(m.content ?? '').length > 100 ? '...' : ''}`);
|
|
349
|
+
log('');
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
function printContactsList(contacts) {
|
|
353
|
+
log(`Found ${contacts.length} contacts:\n`);
|
|
354
|
+
for (const c of contacts) {
|
|
355
|
+
const contact = c;
|
|
356
|
+
log(`👤 ${contact.displayName}`);
|
|
357
|
+
if (contact.email)
|
|
358
|
+
log(` Email: ${contact.email}`);
|
|
359
|
+
if (contact.jobTitle)
|
|
360
|
+
log(` Title: ${contact.jobTitle}`);
|
|
361
|
+
if (contact.department)
|
|
362
|
+
log(` Dept: ${contact.department}`);
|
|
363
|
+
log('');
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
function printActivityList(activities) {
|
|
367
|
+
log(`Found ${activities.length} activity items:\n`);
|
|
368
|
+
const typeIcons = {
|
|
369
|
+
mention: '📣',
|
|
370
|
+
reaction: '👍',
|
|
371
|
+
reply: '💬',
|
|
372
|
+
message: '📝',
|
|
373
|
+
unknown: '❓',
|
|
374
|
+
};
|
|
375
|
+
for (const a of activities) {
|
|
376
|
+
const activity = a;
|
|
377
|
+
const type = activity.type || 'unknown';
|
|
378
|
+
const icon = typeIcons[type] || '❓';
|
|
379
|
+
const sender = activity.sender?.displayName || 'Unknown';
|
|
380
|
+
const time = activity.timestamp ? new Date(activity.timestamp).toLocaleString() : '';
|
|
381
|
+
const topic = activity.topic ? ` in "${activity.topic}"` : '';
|
|
382
|
+
const preview = String(activity.content ?? '').substring(0, 80).replace(/\n/g, ' ');
|
|
383
|
+
log(`${icon} [${type}] ${sender}${topic} - ${time}`);
|
|
384
|
+
log(` ${preview}${String(activity.content ?? '').length > 80 ? '...' : ''}`);
|
|
385
|
+
if (activity.conversationId)
|
|
386
|
+
log(` ConversationId: ${activity.conversationId}`);
|
|
387
|
+
log('');
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
function printProfile(profile) {
|
|
391
|
+
log('👤 Profile:\n');
|
|
392
|
+
for (const [key, value] of Object.entries(profile)) {
|
|
393
|
+
if (value !== null && value !== undefined) {
|
|
394
|
+
log(` ${key}: ${value}`);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
function printGenericResponse(response) {
|
|
399
|
+
// Filter out success flag and print remaining fields
|
|
400
|
+
for (const [key, value] of Object.entries(response)) {
|
|
401
|
+
if (key === 'success')
|
|
402
|
+
continue;
|
|
403
|
+
if (typeof value === 'object' && value !== null) {
|
|
404
|
+
log(`${key}: ${JSON.stringify(value, null, 2)}`);
|
|
405
|
+
}
|
|
406
|
+
else {
|
|
407
|
+
log(`${key}: ${value}`);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
function extractSenderName(sender) {
|
|
412
|
+
if (!sender)
|
|
413
|
+
return null;
|
|
414
|
+
if (typeof sender === 'string')
|
|
415
|
+
return sender;
|
|
416
|
+
if (typeof sender === 'object') {
|
|
417
|
+
const s = sender;
|
|
418
|
+
// Handle { EmailAddress: { Name: string } } structure
|
|
419
|
+
if (s.EmailAddress && typeof s.EmailAddress === 'object') {
|
|
420
|
+
const email = s.EmailAddress;
|
|
421
|
+
if (email.Name)
|
|
422
|
+
return String(email.Name);
|
|
423
|
+
if (email.Address)
|
|
424
|
+
return String(email.Address);
|
|
425
|
+
}
|
|
426
|
+
// Handle { name: string } or { displayName: string } structure
|
|
427
|
+
if (s.displayName)
|
|
428
|
+
return String(s.displayName);
|
|
429
|
+
if (s.name)
|
|
430
|
+
return String(s.name);
|
|
431
|
+
if (s.Name)
|
|
432
|
+
return String(s.Name);
|
|
433
|
+
}
|
|
434
|
+
return null;
|
|
435
|
+
}
|
|
436
|
+
async function main() {
|
|
437
|
+
const parsed = parseArgs();
|
|
438
|
+
if (!parsed.json) {
|
|
439
|
+
console.log('\n🧪 MCP Protocol Test Harness');
|
|
440
|
+
console.log('============================');
|
|
441
|
+
}
|
|
442
|
+
let cleanup = null;
|
|
443
|
+
try {
|
|
444
|
+
const { client, cleanup: cleanupFn } = await createTestClient();
|
|
445
|
+
cleanup = cleanupFn;
|
|
446
|
+
if (!parsed.json) {
|
|
447
|
+
log('\n✅ Connected to MCP server via in-memory transport');
|
|
448
|
+
}
|
|
449
|
+
if (parsed.command === 'list' || !parsed.toolName) {
|
|
450
|
+
await listTools(client, parsed.json);
|
|
451
|
+
}
|
|
452
|
+
else {
|
|
453
|
+
await callTool(client, parsed.toolName, parsed.args, parsed.json);
|
|
454
|
+
}
|
|
455
|
+
if (!parsed.json) {
|
|
456
|
+
logSection('Complete');
|
|
457
|
+
log('MCP protocol test finished successfully.');
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
catch (error) {
|
|
461
|
+
console.error('\n❌ Error:', error instanceof Error ? error.message : String(error));
|
|
462
|
+
if (error instanceof Error && error.stack) {
|
|
463
|
+
console.error('\nStack trace:');
|
|
464
|
+
console.error(error.stack);
|
|
465
|
+
}
|
|
466
|
+
process.exit(1);
|
|
467
|
+
}
|
|
468
|
+
finally {
|
|
469
|
+
if (cleanup) {
|
|
470
|
+
await cleanup();
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
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,191 @@
|
|
|
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, getMessageAuthStatus, 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
|
+
/** Minimum minutes remaining on token to consider it valid (skip browser). */
|
|
43
|
+
const TOKEN_VALID_THRESHOLD_MINUTES = 10;
|
|
44
|
+
async function handleLogin(input, ctx) {
|
|
45
|
+
// Close existing browser if any
|
|
46
|
+
const existingManager = ctx.server.getBrowserManager();
|
|
47
|
+
if (existingManager) {
|
|
48
|
+
await closeBrowser(existingManager, !input.forceNew);
|
|
49
|
+
ctx.server.resetBrowserState();
|
|
50
|
+
}
|
|
51
|
+
if (input.forceNew) {
|
|
52
|
+
clearSessionState();
|
|
53
|
+
clearTokenCache();
|
|
54
|
+
}
|
|
55
|
+
// Fast path: if tokens are still valid, skip browser entirely
|
|
56
|
+
// This is more reliable than browser-based auth detection
|
|
57
|
+
if (!input.forceNew) {
|
|
58
|
+
const tokenStatus = getSubstrateTokenStatus();
|
|
59
|
+
if (tokenStatus.hasToken &&
|
|
60
|
+
tokenStatus.minutesRemaining !== undefined &&
|
|
61
|
+
tokenStatus.minutesRemaining >= TOKEN_VALID_THRESHOLD_MINUTES) {
|
|
62
|
+
ctx.server.markInitialised();
|
|
63
|
+
return {
|
|
64
|
+
success: true,
|
|
65
|
+
data: {
|
|
66
|
+
message: `Already authenticated. Token valid for ${tokenStatus.minutesRemaining} more minutes.`,
|
|
67
|
+
tokenStatus: {
|
|
68
|
+
expiresAt: tokenStatus.expiresAt,
|
|
69
|
+
minutesRemaining: tokenStatus.minutesRemaining,
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
// Smart headless strategy:
|
|
76
|
+
// 1. No session or session too old → visible browser (definitely need login)
|
|
77
|
+
// 2. Session exists and is recent (< 12 hours) → try headless first (SSO likely)
|
|
78
|
+
// 3. forceNew requested → visible browser (user wants fresh login)
|
|
79
|
+
const hasRecentSession = hasSessionState() && !isSessionLikelyExpired();
|
|
80
|
+
const shouldTryHeadless = hasRecentSession && !input.forceNew;
|
|
81
|
+
if (shouldTryHeadless) {
|
|
82
|
+
// Try headless first - SSO may complete silently
|
|
83
|
+
const headlessManager = await createBrowserContext({ headless: true });
|
|
84
|
+
ctx.server.setBrowserManager(headlessManager);
|
|
85
|
+
try {
|
|
86
|
+
await ensureAuthenticated(headlessManager.page, headlessManager.context, (msg) => console.error(`[login:headless] ${msg}`), false, // No overlay in headless
|
|
87
|
+
true // Headless mode - throw immediately if user interaction required
|
|
88
|
+
);
|
|
89
|
+
// If ensureAuthenticated completed without throwing, we're authenticated
|
|
90
|
+
// (it would have thrown or hung if stuck on login page)
|
|
91
|
+
await closeBrowser(headlessManager, true);
|
|
92
|
+
ctx.server.resetBrowserState();
|
|
93
|
+
ctx.server.markInitialised();
|
|
94
|
+
return {
|
|
95
|
+
success: true,
|
|
96
|
+
data: {
|
|
97
|
+
message: 'Login completed silently via SSO. Session has been saved.',
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
// Headless attempt failed - fall through to visible browser
|
|
103
|
+
console.error(`[login:headless] Headless SSO failed, falling back to visible browser: ${error instanceof Error ? error.message : String(error)}`);
|
|
104
|
+
try {
|
|
105
|
+
await closeBrowser(headlessManager, false);
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
// Ignore cleanup errors
|
|
109
|
+
}
|
|
110
|
+
ctx.server.resetBrowserState();
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
// Open visible browser for user interaction
|
|
114
|
+
const browserManager = await createBrowserContext({ headless: false });
|
|
115
|
+
ctx.server.setBrowserManager(browserManager);
|
|
116
|
+
try {
|
|
117
|
+
if (input.forceNew) {
|
|
118
|
+
await forceNewLogin(browserManager.page, browserManager.context, (msg) => console.error(`[login] ${msg}`));
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
await ensureAuthenticated(browserManager.page, browserManager.context, (msg) => console.error(`[login] ${msg}`));
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
finally {
|
|
125
|
+
// Close browser after login - we only need the saved session/tokens
|
|
126
|
+
await closeBrowser(browserManager, true);
|
|
127
|
+
ctx.server.resetBrowserState();
|
|
128
|
+
}
|
|
129
|
+
ctx.server.markInitialised();
|
|
130
|
+
return {
|
|
131
|
+
success: true,
|
|
132
|
+
data: {
|
|
133
|
+
message: 'Login completed successfully. Session has been saved.',
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
async function handleStatus(_input, ctx) {
|
|
138
|
+
const sessionExists = hasSessionState();
|
|
139
|
+
const sessionExpired = isSessionLikelyExpired();
|
|
140
|
+
const tokenStatus = getSubstrateTokenStatus();
|
|
141
|
+
const messageAuthStatus = getMessageAuthStatus();
|
|
142
|
+
const messageAuth = extractMessageAuth();
|
|
143
|
+
const csaToken = extractCsaToken();
|
|
144
|
+
let authStatus = null;
|
|
145
|
+
const browserManager = ctx.server.getBrowserManager();
|
|
146
|
+
if (browserManager && ctx.server.isInitialisedState()) {
|
|
147
|
+
authStatus = await getAuthStatus(browserManager.page);
|
|
148
|
+
}
|
|
149
|
+
return {
|
|
150
|
+
success: true,
|
|
151
|
+
data: {
|
|
152
|
+
directApi: {
|
|
153
|
+
available: tokenStatus.hasToken,
|
|
154
|
+
expiresAt: tokenStatus.expiresAt,
|
|
155
|
+
minutesRemaining: tokenStatus.minutesRemaining,
|
|
156
|
+
},
|
|
157
|
+
messaging: {
|
|
158
|
+
available: messageAuthStatus.hasToken,
|
|
159
|
+
expiresAt: messageAuthStatus.expiresAt,
|
|
160
|
+
minutesRemaining: messageAuthStatus.minutesRemaining,
|
|
161
|
+
},
|
|
162
|
+
favorites: {
|
|
163
|
+
available: messageAuth !== null && csaToken !== null,
|
|
164
|
+
},
|
|
165
|
+
session: {
|
|
166
|
+
exists: sessionExists,
|
|
167
|
+
likelyExpired: sessionExpired,
|
|
168
|
+
},
|
|
169
|
+
browser: {
|
|
170
|
+
running: browserManager !== null,
|
|
171
|
+
initialised: ctx.server.isInitialisedState(),
|
|
172
|
+
},
|
|
173
|
+
authentication: authStatus,
|
|
174
|
+
},
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
178
|
+
// Exports
|
|
179
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
180
|
+
export const loginTool = {
|
|
181
|
+
definition: loginToolDefinition,
|
|
182
|
+
schema: LoginInputSchema,
|
|
183
|
+
handler: handleLogin,
|
|
184
|
+
};
|
|
185
|
+
export const statusTool = {
|
|
186
|
+
definition: statusToolDefinition,
|
|
187
|
+
schema: z.object({}),
|
|
188
|
+
handler: handleStatus,
|
|
189
|
+
};
|
|
190
|
+
/** All auth-related tools. */
|
|
191
|
+
export const authTools = [loginTool, statusTool];
|