msteams-mcp 0.29.1 → 0.29.2
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 +2 -2
- package/dist/auth/session-store.js +4 -9
- package/dist/browser/auth.d.ts +14 -0
- package/dist/browser/auth.js +12 -40
- package/dist/browser/auth.test.d.ts +1 -0
- package/dist/browser/auth.test.js +45 -0
- package/dist/cli.js +0 -0
- package/dist/constants.d.ts +9 -0
- package/dist/constants.js +15 -0
- package/dist/index.js +0 -0
- package/dist/tools/auth-tools.js +30 -1
- package/dist/tools/auth-tools.test.d.ts +1 -0
- package/dist/tools/auth-tools.test.js +67 -0
- package/dist/types/server.d.ts +27 -0
- package/dist/types/server.js +7 -0
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
[](https://www.npmjs.com/package/msteams-mcp)
|
|
5
5
|
[](https://www.npmjs.com/package/msteams-mcp)
|
|
6
6
|
[](https://opensource.org/licenses/MIT)
|
|
7
|
-
[](https://nodejs.org/)
|
|
8
8
|
|
|
9
9
|
Give AI access to Microsoft Teams easily with your standard Teams login.
|
|
10
10
|
|
|
@@ -27,7 +27,7 @@ Search messages and email, catch up on unread chats, read threads, send replies,
|
|
|
27
27
|
|
|
28
28
|
### Prerequisites
|
|
29
29
|
|
|
30
|
-
- Node.js
|
|
30
|
+
- Node.js 20+
|
|
31
31
|
- A Microsoft account with Teams access
|
|
32
32
|
- Google Chrome, Microsoft Edge, or Chromium browser installed
|
|
33
33
|
|
|
@@ -13,7 +13,7 @@ import * as fs from 'fs';
|
|
|
13
13
|
import * as os from 'os';
|
|
14
14
|
import * as path from 'path';
|
|
15
15
|
import { encrypt, decrypt } from './crypto.js';
|
|
16
|
-
import { SESSION_EXPIRY_HOURS } from '../constants.js';
|
|
16
|
+
import { SESSION_EXPIRY_HOURS, TEAMS_HOSTS } from '../constants.js';
|
|
17
17
|
import * as log from '../utils/logger.js';
|
|
18
18
|
/**
|
|
19
19
|
* User-specific config directory for teams-mcp-server.
|
|
@@ -131,12 +131,7 @@ export function clearTokenCache() {
|
|
|
131
131
|
* Known Teams origins (commercial and government clouds).
|
|
132
132
|
* Used to find the correct origin in session state.
|
|
133
133
|
*/
|
|
134
|
-
const TEAMS_ORIGINS =
|
|
135
|
-
'https://teams.cloud.microsoft', // New Teams URL (often holds SubstrateSearch tokens)
|
|
136
|
-
'https://teams.microsoft.com', // Commercial legacy
|
|
137
|
-
'https://teams.microsoft.us', // GCC-High
|
|
138
|
-
'https://dod.teams.microsoft.us', // DoD
|
|
139
|
-
];
|
|
134
|
+
const TEAMS_ORIGINS = TEAMS_HOSTS.map(host => `https://${host}`);
|
|
140
135
|
/**
|
|
141
136
|
* Returns true if localStorage has a SubstrateSearch MSAL token entry.
|
|
142
137
|
*/
|
|
@@ -177,6 +172,6 @@ export function getTeamsOrigin(state) {
|
|
|
177
172
|
if (origin)
|
|
178
173
|
return origin;
|
|
179
174
|
}
|
|
180
|
-
// Fallback:
|
|
181
|
-
return state.origins.find(o =>
|
|
175
|
+
// Fallback: any origin on a known Teams host
|
|
176
|
+
return state.origins.find(o => TEAMS_HOSTS.some(host => o.origin.includes(host))) ?? null;
|
|
182
177
|
}
|
package/dist/browser/auth.d.ts
CHANGED
|
@@ -3,6 +3,19 @@
|
|
|
3
3
|
* Manages login detection and manual authentication flows.
|
|
4
4
|
*/
|
|
5
5
|
import type { Page, BrowserContext } from 'playwright';
|
|
6
|
+
interface AuthStatus {
|
|
7
|
+
isAuthenticated: boolean;
|
|
8
|
+
isOnLoginPage: boolean;
|
|
9
|
+
currentUrl: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Gets the current authentication status.
|
|
13
|
+
*/
|
|
14
|
+
export declare function getAuthStatus(page: Page): Promise<AuthStatus>;
|
|
15
|
+
/**
|
|
16
|
+
* Checks if a URL is a Teams domain.
|
|
17
|
+
*/
|
|
18
|
+
export declare function isTeamsUrl(url: string): boolean;
|
|
6
19
|
/**
|
|
7
20
|
* Performs a full authentication flow:
|
|
8
21
|
* 1. Navigate to Teams
|
|
@@ -16,3 +29,4 @@ import type { Page, BrowserContext } from 'playwright';
|
|
|
16
29
|
* @param headless - If true, throw immediately if user interaction is required (default: false)
|
|
17
30
|
*/
|
|
18
31
|
export declare function ensureAuthenticated(page: Page, context: BrowserContext, onProgress?: (message: string) => void, showOverlay?: boolean, headless?: boolean): Promise<void>;
|
|
32
|
+
export {};
|
package/dist/browser/auth.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Manages login detection and manual authentication flows.
|
|
4
4
|
*/
|
|
5
5
|
import { saveSessionState } from './context.js';
|
|
6
|
-
import { OVERLAY_STEP_PAUSE_MS, OVERLAY_COMPLETE_PAUSE_MS, } from '../constants.js';
|
|
6
|
+
import { OVERLAY_STEP_PAUSE_MS, OVERLAY_COMPLETE_PAUSE_MS, TEAMS_HOSTS, } from '../constants.js';
|
|
7
7
|
import { extractSubstrateToken } from '../auth/token-extractor.js';
|
|
8
8
|
import * as logger from '../utils/logger.js';
|
|
9
9
|
/**
|
|
@@ -145,41 +145,16 @@ const LOGIN_URL_PATTERNS = [
|
|
|
145
145
|
'login.live.com',
|
|
146
146
|
'login.microsoft.com',
|
|
147
147
|
];
|
|
148
|
-
// Selectors that indicate successful authentication
|
|
149
|
-
const AUTH_SUCCESS_SELECTORS = [
|
|
150
|
-
'[data-tid="app-bar"]',
|
|
151
|
-
'[data-tid="search-box"]',
|
|
152
|
-
'input[placeholder*="Search"]',
|
|
153
|
-
'[data-tid="chat-list"]',
|
|
154
|
-
'[data-tid="team-list"]',
|
|
155
|
-
];
|
|
156
148
|
/**
|
|
157
149
|
* Checks if the current page URL indicates a login flow.
|
|
158
150
|
*/
|
|
159
151
|
function isLoginUrl(url) {
|
|
160
152
|
return LOGIN_URL_PATTERNS.some(pattern => url.includes(pattern));
|
|
161
153
|
}
|
|
162
|
-
/**
|
|
163
|
-
* Checks if the page shows authenticated Teams content.
|
|
164
|
-
*/
|
|
165
|
-
async function hasAuthenticatedContent(page) {
|
|
166
|
-
for (const selector of AUTH_SUCCESS_SELECTORS) {
|
|
167
|
-
try {
|
|
168
|
-
const count = await page.locator(selector).count();
|
|
169
|
-
if (count > 0) {
|
|
170
|
-
return true;
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
catch {
|
|
174
|
-
// Selector not found, continue checking others
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
return false;
|
|
178
|
-
}
|
|
179
154
|
/**
|
|
180
155
|
* Gets the current authentication status.
|
|
181
156
|
*/
|
|
182
|
-
async function getAuthStatus(page) {
|
|
157
|
+
export async function getAuthStatus(page) {
|
|
183
158
|
const currentUrl = page.url();
|
|
184
159
|
const onLoginPage = isLoginUrl(currentUrl);
|
|
185
160
|
// If on login page, definitely not authenticated
|
|
@@ -190,11 +165,15 @@ async function getAuthStatus(page) {
|
|
|
190
165
|
currentUrl,
|
|
191
166
|
};
|
|
192
167
|
}
|
|
193
|
-
//
|
|
194
|
-
|
|
195
|
-
|
|
168
|
+
// On a Teams domain, a live MSAL token is the proof of sign-in: it is what
|
|
169
|
+
// every API call needs and what the saved session is for. Teams' own DOM
|
|
170
|
+
// markers are not a usable signal — new Teams renders none of the `data-tid`
|
|
171
|
+
// elements earlier versions exposed, so waiting for them left a completed
|
|
172
|
+
// login undetected and the browser window open until the manual-login
|
|
173
|
+
// timeout.
|
|
174
|
+
if (isTeamsUrl(currentUrl)) {
|
|
196
175
|
return {
|
|
197
|
-
isAuthenticated:
|
|
176
|
+
isAuthenticated: await checkBrowserTokenExpiry(page) > 0,
|
|
198
177
|
isOnLoginPage: false,
|
|
199
178
|
currentUrl,
|
|
200
179
|
};
|
|
@@ -208,18 +187,11 @@ async function getAuthStatus(page) {
|
|
|
208
187
|
}
|
|
209
188
|
/** Timeout for detecting login redirect (ms). */
|
|
210
189
|
const LOGIN_REDIRECT_TIMEOUT_MS = 5000;
|
|
211
|
-
/** URL patterns that indicate we're on a Teams page (not redirected elsewhere). */
|
|
212
|
-
const TEAMS_URL_PATTERNS = [
|
|
213
|
-
'teams.microsoft.com',
|
|
214
|
-
'teams.microsoft.us', // GCC-High
|
|
215
|
-
'dod.teams.microsoft.us', // DoD
|
|
216
|
-
'teams.cloud.microsoft', // New Teams URL
|
|
217
|
-
];
|
|
218
190
|
/**
|
|
219
191
|
* Checks if a URL is a Teams domain.
|
|
220
192
|
*/
|
|
221
|
-
function isTeamsUrl(url) {
|
|
222
|
-
return
|
|
193
|
+
export function isTeamsUrl(url) {
|
|
194
|
+
return TEAMS_HOSTS.some(host => url.includes(host));
|
|
223
195
|
}
|
|
224
196
|
/**
|
|
225
197
|
* Navigates to Teams and checks authentication status.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
vi.mock('./context.js', () => ({ saveSessionState: vi.fn() }));
|
|
3
|
+
vi.mock('../auth/token-extractor.js', () => ({ extractSubstrateToken: vi.fn() }));
|
|
4
|
+
import { getAuthStatus, isTeamsUrl } from './auth.js';
|
|
5
|
+
/** A page on `url` with no Teams DOM markers and `tokenMinutes` left on MSAL. */
|
|
6
|
+
function fakePage(url, tokenMinutes) {
|
|
7
|
+
return {
|
|
8
|
+
url: () => url,
|
|
9
|
+
locator: () => ({ count: async () => 0 }),
|
|
10
|
+
evaluate: async () => tokenMinutes,
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
describe('isTeamsUrl', () => {
|
|
14
|
+
it('covers every host Microsoft redirects Teams users to', () => {
|
|
15
|
+
// New Teams settles on the bare host, with no path.
|
|
16
|
+
for (const url of [
|
|
17
|
+
'https://teams.microsoft.com/',
|
|
18
|
+
'https://teams.cloud.microsoft/',
|
|
19
|
+
'https://teams.chat.microsoft.com/',
|
|
20
|
+
'https://teams.microsoft.us/',
|
|
21
|
+
'https://dod.teams.microsoft.us/',
|
|
22
|
+
]) {
|
|
23
|
+
expect(isTeamsUrl(url), url).toBe(true);
|
|
24
|
+
}
|
|
25
|
+
expect(isTeamsUrl('https://outlook.office.com/')).toBe(false);
|
|
26
|
+
});
|
|
27
|
+
it('matches on the host, whatever path the app has routed to', () => {
|
|
28
|
+
expect(isTeamsUrl('https://teams.microsoft.com/v2/#/conversations/19:abc@thread.tacv2')).toBe(true);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
describe('getAuthStatus', () => {
|
|
32
|
+
it('detects a completed login from the MSAL token alone', async () => {
|
|
33
|
+
const status = await getAuthStatus(fakePage('https://teams.cloud.microsoft/', 55));
|
|
34
|
+
expect(status.isAuthenticated).toBe(true);
|
|
35
|
+
expect(status.isOnLoginPage).toBe(false);
|
|
36
|
+
});
|
|
37
|
+
it('stays unauthenticated on a Teams host with no token', async () => {
|
|
38
|
+
const status = await getAuthStatus(fakePage('https://teams.cloud.microsoft/', -1));
|
|
39
|
+
expect(status.isAuthenticated).toBe(false);
|
|
40
|
+
});
|
|
41
|
+
it('reports the login page', async () => {
|
|
42
|
+
const status = await getAuthStatus(fakePage('https://login.microsoftonline.com/common/oauth2', 99));
|
|
43
|
+
expect(status).toMatchObject({ isAuthenticated: false, isOnLoginPage: true });
|
|
44
|
+
});
|
|
45
|
+
});
|
package/dist/cli.js
CHANGED
|
File without changes
|
package/dist/constants.d.ts
CHANGED
|
@@ -101,3 +101,12 @@ export declare const STANDARD_EMOJIS: readonly StandardEmoji[];
|
|
|
101
101
|
export declare const ASSIGNMENTS_APP_ID = "8f348934-64be-4bb2-bc16-c54c96789f43";
|
|
102
102
|
/** Microsoft Graph token audiences: MSAL caches either the URL or the app ID. */
|
|
103
103
|
export declare const GRAPH_AUDIENCES: readonly string[];
|
|
104
|
+
/**
|
|
105
|
+
* Teams web hosts, in the order a session's origins should be preferred.
|
|
106
|
+
*
|
|
107
|
+
* Microsoft keeps adding hosts (`teams.cloud.microsoft`, then
|
|
108
|
+
* `teams.chat.microsoft.com`) and redirects users to them without warning, so
|
|
109
|
+
* every host check reads this one list: a missing host makes login detection
|
|
110
|
+
* fail on a page that is in fact signed in.
|
|
111
|
+
*/
|
|
112
|
+
export declare const TEAMS_HOSTS: readonly string[];
|
package/dist/constants.js
CHANGED
|
@@ -192,3 +192,18 @@ export const STANDARD_EMOJIS = [
|
|
|
192
192
|
export const ASSIGNMENTS_APP_ID = '8f348934-64be-4bb2-bc16-c54c96789f43';
|
|
193
193
|
/** Microsoft Graph token audiences: MSAL caches either the URL or the app ID. */
|
|
194
194
|
export const GRAPH_AUDIENCES = ['https://graph.microsoft.com', '00000003-0000-0000-c000-000000000000'];
|
|
195
|
+
/**
|
|
196
|
+
* Teams web hosts, in the order a session's origins should be preferred.
|
|
197
|
+
*
|
|
198
|
+
* Microsoft keeps adding hosts (`teams.cloud.microsoft`, then
|
|
199
|
+
* `teams.chat.microsoft.com`) and redirects users to them without warning, so
|
|
200
|
+
* every host check reads this one list: a missing host makes login detection
|
|
201
|
+
* fail on a page that is in fact signed in.
|
|
202
|
+
*/
|
|
203
|
+
export const TEAMS_HOSTS = [
|
|
204
|
+
'teams.cloud.microsoft', // New Teams (often holds the SubstrateSearch token)
|
|
205
|
+
'teams.chat.microsoft.com', // New Teams chat host
|
|
206
|
+
'teams.microsoft.com', // Commercial legacy
|
|
207
|
+
'teams.microsoft.us', // GCC-High
|
|
208
|
+
'dod.teams.microsoft.us', // DoD
|
|
209
|
+
];
|
package/dist/index.js
CHANGED
|
File without changes
|
package/dist/tools/auth-tools.js
CHANGED
|
@@ -6,7 +6,8 @@ import { z } from 'zod';
|
|
|
6
6
|
const require = createRequire(import.meta.url);
|
|
7
7
|
const pkg = require('../../package.json');
|
|
8
8
|
import { hasSessionState, isSessionLikelyExpired, clearSessionState, clearTokenCache, } from '../auth/session-store.js';
|
|
9
|
-
import { getSubstrateTokenStatus, getMessageAuthStatus, extractMessageAuth, extractCsaToken, } from '../auth/token-extractor.js';
|
|
9
|
+
import { getSubstrateTokenStatus, getMessageAuthStatus, extractMessageAuth, extractCsaToken, extractSubstrateToken, extractSkypeSpacesToken, } from '../auth/token-extractor.js';
|
|
10
|
+
import { refreshTokensViaHttp } from '../auth/token-refresh-http.js';
|
|
10
11
|
import { createBrowserContext, closeBrowser } from '../browser/context.js';
|
|
11
12
|
import * as log from '../utils/logger.js';
|
|
12
13
|
import { resetAssignmentsAvailability } from '../utils/auth-guards.js';
|
|
@@ -46,6 +47,31 @@ const statusToolDefinition = {
|
|
|
46
47
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
47
48
|
/** Minimum minutes remaining on token to consider it valid (skip browser). */
|
|
48
49
|
const TOKEN_VALID_THRESHOLD_MINUTES = 10;
|
|
50
|
+
/** Every core credential a session needs: search, calendar, favourites, messaging. */
|
|
51
|
+
function coreCredentialsComplete() {
|
|
52
|
+
return extractSubstrateToken() !== null
|
|
53
|
+
&& extractSkypeSpacesToken() !== null
|
|
54
|
+
&& extractCsaToken() !== null
|
|
55
|
+
&& extractMessageAuth() !== null;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Mints any core credential the browser did not leave in the saved session.
|
|
59
|
+
*
|
|
60
|
+
* Teams JS acquires some resource tokens lazily — the CSA (chatsvcagg) token
|
|
61
|
+
* behind favourites is routinely absent from a session saved straight after
|
|
62
|
+
* SSO, so login would report success while teams_status reported favourites
|
|
63
|
+
* unavailable until some later call happened to trigger a refresh. One HTTP
|
|
64
|
+
* exchange mints every core resource. Failure is non-fatal: the session is
|
|
65
|
+
* still usable and the next tool call retries through auto-login.
|
|
66
|
+
*/
|
|
67
|
+
async function fillCoreCredentialGaps() {
|
|
68
|
+
if (coreCredentialsComplete())
|
|
69
|
+
return;
|
|
70
|
+
const result = await refreshTokensViaHttp();
|
|
71
|
+
if (!result.ok) {
|
|
72
|
+
log.warn('login', `Could not top up core credentials: ${result.error.message}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
49
75
|
async function handleLogin(input) {
|
|
50
76
|
// A fresh login may have changed what the account can access (e.g. new consent)
|
|
51
77
|
resetAssignmentsAvailability();
|
|
@@ -57,6 +83,7 @@ async function handleLogin(input) {
|
|
|
57
83
|
// Fast path: if tokens are still valid, skip browser entirely
|
|
58
84
|
const tokenStatus = getSubstrateTokenStatus();
|
|
59
85
|
if (tokenStatus.hasToken && tokenStatus.minutesRemaining >= TOKEN_VALID_THRESHOLD_MINUTES) {
|
|
86
|
+
await fillCoreCredentialGaps();
|
|
60
87
|
return {
|
|
61
88
|
success: true,
|
|
62
89
|
data: {
|
|
@@ -83,6 +110,7 @@ async function handleLogin(input) {
|
|
|
83
110
|
true // Headless mode - throw immediately if user interaction required
|
|
84
111
|
);
|
|
85
112
|
await closeBrowser(headless, true);
|
|
113
|
+
await fillCoreCredentialGaps();
|
|
86
114
|
return {
|
|
87
115
|
success: true,
|
|
88
116
|
data: {
|
|
@@ -103,6 +131,7 @@ async function handleLogin(input) {
|
|
|
103
131
|
// Close browser after login - we only need the saved session/tokens
|
|
104
132
|
await closeBrowser(visible, true);
|
|
105
133
|
}
|
|
134
|
+
await fillCoreCredentialGaps();
|
|
106
135
|
return {
|
|
107
136
|
success: true,
|
|
108
137
|
data: {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
vi.mock('../auth/session-store.js', () => ({
|
|
3
|
+
hasSessionState: vi.fn(() => true),
|
|
4
|
+
isSessionLikelyExpired: vi.fn(() => false),
|
|
5
|
+
clearSessionState: vi.fn(),
|
|
6
|
+
clearTokenCache: vi.fn(),
|
|
7
|
+
}));
|
|
8
|
+
vi.mock('../auth/token-extractor.js', () => ({
|
|
9
|
+
getSubstrateTokenStatus: vi.fn(),
|
|
10
|
+
getMessageAuthStatus: vi.fn(),
|
|
11
|
+
extractMessageAuth: vi.fn(),
|
|
12
|
+
extractCsaToken: vi.fn(),
|
|
13
|
+
extractSubstrateToken: vi.fn(),
|
|
14
|
+
extractSkypeSpacesToken: vi.fn(),
|
|
15
|
+
}));
|
|
16
|
+
vi.mock('../auth/token-refresh-http.js', () => ({ refreshTokensViaHttp: vi.fn() }));
|
|
17
|
+
vi.mock('../browser/context.js', () => ({ createBrowserContext: vi.fn(), closeBrowser: vi.fn() }));
|
|
18
|
+
vi.mock('../browser/auth.js', () => ({ ensureAuthenticated: vi.fn() }));
|
|
19
|
+
vi.mock('../utils/auth-guards.js', () => ({ resetAssignmentsAvailability: vi.fn() }));
|
|
20
|
+
import { loginTool } from './auth-tools.js';
|
|
21
|
+
import { getSubstrateTokenStatus, extractMessageAuth, extractCsaToken, extractSubstrateToken, extractSkypeSpacesToken, } from '../auth/token-extractor.js';
|
|
22
|
+
import { refreshTokensViaHttp } from '../auth/token-refresh-http.js';
|
|
23
|
+
import { createBrowserContext, closeBrowser } from '../browser/context.js';
|
|
24
|
+
/** A session holding every core credential except the CSA (favourites) token. */
|
|
25
|
+
function sessionMissingCsaToken() {
|
|
26
|
+
vi.mocked(getSubstrateTokenStatus).mockReturnValue({ hasToken: true, minutesRemaining: 60 });
|
|
27
|
+
vi.mocked(extractSubstrateToken).mockReturnValue({ token: 'substrate', expiry: new Date(Date.now() + 3.6e6) });
|
|
28
|
+
vi.mocked(extractSkypeSpacesToken).mockReturnValue('spaces');
|
|
29
|
+
vi.mocked(extractMessageAuth).mockReturnValue({ skypeToken: 's', authToken: 'a', userMri: '8:orgid:u' });
|
|
30
|
+
vi.mocked(extractCsaToken).mockReturnValue(null);
|
|
31
|
+
}
|
|
32
|
+
describe('teams_login core credential top-up', () => {
|
|
33
|
+
beforeEach(() => {
|
|
34
|
+
vi.clearAllMocks();
|
|
35
|
+
vi.mocked(refreshTokensViaHttp).mockResolvedValue({ ok: true, value: undefined });
|
|
36
|
+
});
|
|
37
|
+
it('mints the missing CSA token when the session is otherwise valid', async () => {
|
|
38
|
+
sessionMissingCsaToken();
|
|
39
|
+
const result = await loginTool.handler({ forceNew: false });
|
|
40
|
+
expect(result.success).toBe(true);
|
|
41
|
+
expect(refreshTokensViaHttp).toHaveBeenCalledOnce();
|
|
42
|
+
expect(createBrowserContext).not.toHaveBeenCalled();
|
|
43
|
+
});
|
|
44
|
+
it('skips the refresh when every core credential is present', async () => {
|
|
45
|
+
sessionMissingCsaToken();
|
|
46
|
+
vi.mocked(extractCsaToken).mockReturnValue('csa');
|
|
47
|
+
await loginTool.handler({ forceNew: false });
|
|
48
|
+
expect(refreshTokensViaHttp).not.toHaveBeenCalled();
|
|
49
|
+
});
|
|
50
|
+
it('tops up after a forced browser login leaves the CSA token behind', async () => {
|
|
51
|
+
sessionMissingCsaToken();
|
|
52
|
+
vi.mocked(createBrowserContext).mockResolvedValue({ page: {}, context: { clearCookies: vi.fn() } });
|
|
53
|
+
vi.mocked(closeBrowser).mockResolvedValue(undefined);
|
|
54
|
+
const result = await loginTool.handler({ forceNew: true });
|
|
55
|
+
expect(result.success).toBe(true);
|
|
56
|
+
expect(refreshTokensViaHttp).toHaveBeenCalledOnce();
|
|
57
|
+
});
|
|
58
|
+
it('still reports success when the top-up fails', async () => {
|
|
59
|
+
sessionMissingCsaToken();
|
|
60
|
+
vi.mocked(refreshTokensViaHttp).mockResolvedValue({
|
|
61
|
+
ok: false,
|
|
62
|
+
error: { code: 'NETWORK_ERROR', message: 'offline' },
|
|
63
|
+
});
|
|
64
|
+
const result = await loginTool.handler({ forceNew: false });
|
|
65
|
+
expect(result.success).toBe(true);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server interface types.
|
|
3
|
+
*
|
|
4
|
+
* Extracted to a shared module to avoid circular dependencies between
|
|
5
|
+
* server.ts and tools/index.ts.
|
|
6
|
+
*/
|
|
7
|
+
import type { BrowserManager } from '../browser/context.js';
|
|
8
|
+
/**
|
|
9
|
+
* Interface for the TeamsServer class.
|
|
10
|
+
*
|
|
11
|
+
* This interface is used by tool handlers to interact with the server
|
|
12
|
+
* without creating circular dependencies.
|
|
13
|
+
*/
|
|
14
|
+
export interface TeamsServer {
|
|
15
|
+
/** Ensures a browser is running and authenticated. */
|
|
16
|
+
ensureBrowser(headless?: boolean): Promise<BrowserManager>;
|
|
17
|
+
/** Resets browser state (clears manager and initialisation flag). */
|
|
18
|
+
resetBrowserState(): void;
|
|
19
|
+
/** Gets the current browser manager, if any. */
|
|
20
|
+
getBrowserManager(): BrowserManager | null;
|
|
21
|
+
/** Sets the browser manager. */
|
|
22
|
+
setBrowserManager(manager: BrowserManager): void;
|
|
23
|
+
/** Marks the server as initialised. */
|
|
24
|
+
markInitialised(): void;
|
|
25
|
+
/** Checks if the server is initialised. */
|
|
26
|
+
isInitialisedState(): boolean;
|
|
27
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "msteams-mcp",
|
|
3
|
-
"version": "0.29.
|
|
3
|
+
"version": "0.29.2",
|
|
4
4
|
"description": "MCP server for Microsoft Teams - search messages, send replies, manage favourites",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"url": "https://github.com/m0nkmaster/msteams-mcp/issues"
|
|
22
22
|
},
|
|
23
23
|
"engines": {
|
|
24
|
-
"node": ">=
|
|
24
|
+
"node": ">=20"
|
|
25
25
|
},
|
|
26
26
|
"scripts": {
|
|
27
27
|
"build": "tsc",
|
|
@@ -45,6 +45,7 @@
|
|
|
45
45
|
"license": "MIT",
|
|
46
46
|
"dependencies": {
|
|
47
47
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
48
|
+
"marked": "^18.0.14",
|
|
48
49
|
"playwright": "^1.40.0",
|
|
49
50
|
"zod": "~3.23.0"
|
|
50
51
|
},
|