msteams-mcp 0.3.3 → 0.3.5

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 CHANGED
@@ -10,7 +10,7 @@ This server calls Microsoft's internal Teams APIs directly (Substrate, chatsvc,
10
10
  1. Run `teams_login` to open a browser and log in
11
11
  2. OAuth tokens are extracted and cached
12
12
  3. All operations use cached tokens directly (no browser needed)
13
- 4. When tokens expire (~1 hour), run `teams_login` again
13
+ 4. Automatic token refresh (~1 hour)
14
14
 
15
15
  ## Installation
16
16
 
@@ -111,17 +111,17 @@ The search supports Teams' native operators:
111
111
 
112
112
  ```
113
113
  from:sarah@company.com # Messages from person
114
- sent:today # Messages from today
115
- sent:lastweek # Messages from last week
114
+ sent:2026-01-20 # Messages from specific date
115
+ sent:>=2026-01-15 # Messages since date
116
116
  in:project-alpha # Messages in channel
117
117
  "Rob Smith" # Find @mentions (name in quotes)
118
118
  hasattachment:true # Messages with files
119
119
  NOT from:email@co.com # Exclude results
120
120
  ```
121
121
 
122
- Combine operators: `from:sarah@co.com sent:lastweek hasattachment:true`
122
+ Combine operators: `from:sarah@co.com sent:>=2026-01-18 hasattachment:true`
123
123
 
124
- **Note:** `@me`, `from:me`, `to:me` do NOT work. Use `teams_get_me` first to get your email/displayName, then use those values.
124
+ **Note:** `@me`, `from:me`, `to:me` do NOT work. Use `teams_get_me` first to get your email/displayName. Also `sent:lastweek`, `sent:today`, `sent:thisweek` do NOT work - use explicit dates or omit (results are sorted by recency).
125
125
 
126
126
  ## MCP Resources
127
127
 
@@ -176,7 +176,7 @@ npm run test:mcp -- unread # Check unread counts
176
176
  ## Limitations
177
177
 
178
178
  - **Login required** - Run `teams_login` to authenticate (opens browser)
179
- - **Token expiry** - Tokens expire after ~1 hour; run `teams_login` again when needed
179
+ - **Token expiry** - Tokens expire after ~1 hour; headless refresh is attempted or run `teams_login` again when needed
180
180
  - **Undocumented APIs** - Uses Microsoft's internal APIs which may change without notice
181
181
  - **Search limitations** - Full-text search only; thread replies not matching search terms won't appear (use `teams_get_thread` for full context)
182
182
  - **Own messages only** - Edit/delete only works on your own messages
@@ -8,14 +8,14 @@ import { SUBSTRATE_API, getBearerHeaders } from '../utils/api-config.js';
8
8
  import { ErrorCode } from '../types/errors.js';
9
9
  import { ok } from '../types/result.js';
10
10
  import { clearTokenCache } from '../auth/token-extractor.js';
11
- import { requireSubstrateToken } from '../utils/auth-guards.js';
11
+ import { requireSubstrateTokenAsync } from '../utils/auth-guards.js';
12
12
  import { parseSearchResults, parsePeopleResults, parseChannelResults, filterChannelsByName, } from '../utils/parsers.js';
13
13
  import { getMyTeamsAndChannels } from './csa-api.js';
14
14
  /**
15
15
  * Searches Teams messages using the Substrate v2 query API.
16
16
  */
17
17
  export async function searchMessages(query, options = {}) {
18
- const tokenResult = requireSubstrateToken();
18
+ const tokenResult = await requireSubstrateTokenAsync();
19
19
  if (!tokenResult.ok) {
20
20
  return tokenResult;
21
21
  }
@@ -92,7 +92,7 @@ export async function searchMessages(query, options = {}) {
92
92
  * Searches for people by name or email.
93
93
  */
94
94
  export async function searchPeople(query, limit = 10) {
95
- const tokenResult = requireSubstrateToken();
95
+ const tokenResult = await requireSubstrateTokenAsync();
96
96
  if (!tokenResult.ok) {
97
97
  return tokenResult;
98
98
  }
@@ -143,7 +143,7 @@ export async function searchPeople(query, limit = 10) {
143
143
  * Gets the user's frequently contacted people.
144
144
  */
145
145
  export async function getFrequentContacts(limit = 50) {
146
- const tokenResult = requireSubstrateToken();
146
+ const tokenResult = await requireSubstrateTokenAsync();
147
147
  if (!tokenResult.ok) {
148
148
  return tokenResult;
149
149
  }
@@ -201,7 +201,7 @@ export async function getFrequentContacts(limit = 50) {
201
201
  * @param limit - Maximum number of results (default: 10, max: 50)
202
202
  */
203
203
  export async function searchChannels(query, limit = 10) {
204
- const tokenResult = requireSubstrateToken();
204
+ const tokenResult = await requireSubstrateTokenAsync();
205
205
  if (!tokenResult.ok) {
206
206
  return tokenResult;
207
207
  }
@@ -2,21 +2,23 @@
2
2
  * Token extraction from session state.
3
3
  *
4
4
  * Extracts various authentication tokens from Playwright's saved session state.
5
+ * Teams stores MSAL tokens in localStorage; we parse these to get bearer tokens
6
+ * for various APIs (Substrate search, chatsvc messaging, etc.).
5
7
  */
6
8
  import { clearTokenCache, type SessionState } from './session-store.js';
7
9
  import { type UserProfile } from '../utils/parsers.js';
8
- /** Substrate search token information. */
10
+ /** Substrate search token (for search/people APIs). */
9
11
  export interface SubstrateTokenInfo {
10
12
  token: string;
11
13
  expiry: Date;
12
14
  }
13
- /** Teams API token information. */
15
+ /** Teams chat API token (for chatsvc). */
14
16
  export interface TeamsTokenInfo {
15
17
  token: string;
16
18
  expiry: Date;
17
19
  userMri: string;
18
20
  }
19
- /** Message authentication information (cookies). */
21
+ /** Cookie-based auth for messaging APIs. */
20
22
  export interface MessageAuthInfo {
21
23
  skypeToken: string;
22
24
  authToken: string;
@@ -24,6 +26,7 @@ export interface MessageAuthInfo {
24
26
  }
25
27
  /**
26
28
  * Extracts the Substrate search token from session state.
29
+ * This token is used for search and people APIs.
27
30
  */
28
31
  export declare function extractSubstrateToken(state?: SessionState): SubstrateTokenInfo | null;
29
32
  /**
@@ -44,14 +47,20 @@ export declare function getSubstrateTokenStatus(): {
44
47
  };
45
48
  /**
46
49
  * Extracts the Teams chat API token from session state.
50
+ *
51
+ * Teams stores multiple tokens for different services. We prefer:
52
+ * 1. chatsvcagg.teams.microsoft.com (primary chat API)
53
+ * 2. api.spaces.skype.com (fallback)
47
54
  */
48
55
  export declare function extractTeamsToken(state?: SessionState): TeamsTokenInfo | null;
49
56
  /**
50
- * Extracts authentication info needed for messaging API (uses cookies).
57
+ * Extracts authentication info needed for messaging API.
58
+ * Unlike other APIs, messaging uses cookies rather than localStorage tokens.
51
59
  */
52
60
  export declare function extractMessageAuth(state?: SessionState): MessageAuthInfo | null;
53
61
  /**
54
62
  * Extracts the CSA token for the conversationFolders API.
63
+ * This searches all origins, not just teams.microsoft.com.
55
64
  */
56
65
  export declare function extractCsaToken(state?: SessionState): string | null;
57
66
  /**
@@ -60,6 +69,7 @@ export declare function extractCsaToken(state?: SessionState): string | null;
60
69
  export declare function getUserProfile(state?: SessionState): UserProfile | null;
61
70
  /**
62
71
  * Gets user's display name from session state.
72
+ * Searches localStorage entries first, then falls back to JWT claims.
63
73
  */
64
74
  export declare function getUserDisplayName(state?: SessionState): string | null;
65
75
  /**
@@ -2,12 +2,17 @@
2
2
  * Token extraction from session state.
3
3
  *
4
4
  * Extracts various authentication tokens from Playwright's saved session state.
5
+ * Teams stores MSAL tokens in localStorage; we parse these to get bearer tokens
6
+ * for various APIs (Substrate search, chatsvc messaging, etc.).
5
7
  */
6
8
  import { readSessionState, readTokenCache, writeTokenCache, clearTokenCache, getTeamsOrigin, } from './session-store.js';
7
9
  import { parseJwtProfile } from '../utils/parsers.js';
10
+ import { MRI_ORGID_PREFIX } from '../constants.js';
11
+ // ============================================================================
12
+ // JWT Utilities
13
+ // ============================================================================
8
14
  /**
9
15
  * Decodes a JWT token's payload without verifying the signature.
10
- * Returns null if the token is invalid.
11
16
  */
12
17
  function decodeJwtPayload(token) {
13
18
  try {
@@ -21,7 +26,7 @@ function decodeJwtPayload(token) {
21
26
  }
22
27
  }
23
28
  /**
24
- * Gets the expiry date from a JWT token.
29
+ * Gets the expiry date from a JWT token's `exp` claim.
25
30
  */
26
31
  function getJwtExpiry(token) {
27
32
  const payload = decodeJwtPayload(token);
@@ -30,26 +35,48 @@ function getJwtExpiry(token) {
30
35
  return new Date(payload.exp * 1000);
31
36
  }
32
37
  /**
33
- * Extracts the Substrate search token from session state.
38
+ * Checks if a string looks like a JWT (starts with 'ey').
34
39
  */
35
- export function extractSubstrateToken(state) {
40
+ function isJwtToken(value) {
41
+ return typeof value === 'string' && value.startsWith('ey');
42
+ }
43
+ // ============================================================================
44
+ // Session Helpers
45
+ // ============================================================================
46
+ /**
47
+ * Resolves session state and Teams origin in one call.
48
+ * Many functions need both, so this reduces boilerplate.
49
+ */
50
+ function getTeamsLocalStorage(state) {
36
51
  const sessionState = state ?? readSessionState();
37
52
  if (!sessionState)
38
53
  return null;
39
54
  const teamsOrigin = getTeamsOrigin(sessionState);
40
- if (!teamsOrigin)
55
+ return teamsOrigin?.localStorage ?? null;
56
+ }
57
+ // ============================================================================
58
+ // Token Extraction
59
+ // ============================================================================
60
+ /**
61
+ * Extracts the Substrate search token from session state.
62
+ * This token is used for search and people APIs.
63
+ */
64
+ export function extractSubstrateToken(state) {
65
+ const localStorage = getTeamsLocalStorage(state);
66
+ if (!localStorage)
41
67
  return null;
42
- for (const item of teamsOrigin.localStorage) {
68
+ for (const item of localStorage) {
43
69
  try {
44
- const val = JSON.parse(item.value);
45
- if (val.target?.includes('substrate.office.com/search/SubstrateSearch')) {
46
- const token = val.secret;
47
- if (!token || typeof token !== 'string')
48
- continue;
49
- const expiry = getJwtExpiry(token);
50
- if (expiry) {
51
- return { token, expiry };
52
- }
70
+ const entry = JSON.parse(item.value);
71
+ // Look for the Substrate search token by its target scope
72
+ if (!entry.target?.includes('substrate.office.com/search/SubstrateSearch')) {
73
+ continue;
74
+ }
75
+ if (!isJwtToken(entry.secret))
76
+ continue;
77
+ const expiry = getJwtExpiry(entry.secret);
78
+ if (expiry) {
79
+ return { token: entry.secret, expiry };
53
80
  }
54
81
  }
55
82
  catch {
@@ -58,6 +85,9 @@ export function extractSubstrateToken(state) {
58
85
  }
59
86
  return null;
60
87
  }
88
+ // ============================================================================
89
+ // Cached Token Access
90
+ // ============================================================================
61
91
  /**
62
92
  * Gets a valid Substrate token, either from cache or by extracting from session.
63
93
  */
@@ -108,47 +138,40 @@ export function getSubstrateTokenStatus() {
108
138
  }
109
139
  /**
110
140
  * Extracts the Teams chat API token from session state.
141
+ *
142
+ * Teams stores multiple tokens for different services. We prefer:
143
+ * 1. chatsvcagg.teams.microsoft.com (primary chat API)
144
+ * 2. api.spaces.skype.com (fallback)
111
145
  */
112
146
  export function extractTeamsToken(state) {
113
- const sessionState = state ?? readSessionState();
114
- if (!sessionState)
115
- return null;
116
- const teamsOrigin = getTeamsOrigin(sessionState);
117
- if (!teamsOrigin)
147
+ const localStorage = getTeamsLocalStorage(state);
148
+ if (!localStorage)
118
149
  return null;
119
- let chatToken = null;
120
- let chatTokenExpiry = null;
121
- let skypeToken = null;
122
- let skypeTokenExpiry = null;
150
+ let chatsvcCandidate = null;
151
+ let skypeCandidate = null;
123
152
  let userMri = null;
124
- for (const item of teamsOrigin.localStorage) {
153
+ for (const item of localStorage) {
125
154
  try {
126
- const val = JSON.parse(item.value);
127
- if (!val.target || !val.secret)
128
- continue;
129
- const secret = val.secret;
130
- if (typeof secret !== 'string' || !secret.startsWith('ey'))
155
+ const entry = JSON.parse(item.value);
156
+ if (!entry.target || !isJwtToken(entry.secret))
131
157
  continue;
132
- const payload = decodeJwtPayload(secret);
158
+ const payload = decodeJwtPayload(entry.secret);
133
159
  if (!payload?.exp || typeof payload.exp !== 'number')
134
160
  continue;
135
- const tokenExpiry = new Date(payload.exp * 1000);
136
- // Extract user MRI from any token
161
+ const expiry = new Date(payload.exp * 1000);
162
+ // Capture user MRI from any token's oid claim
137
163
  if (typeof payload.oid === 'string' && !userMri) {
138
- userMri = `8:orgid:${payload.oid}`;
164
+ userMri = `${MRI_ORGID_PREFIX}${payload.oid}`;
139
165
  }
140
- // Prefer chatsvcagg.teams.microsoft.com token
141
- if (val.target.includes('chatsvcagg.teams.microsoft.com')) {
142
- if (!chatTokenExpiry || tokenExpiry > chatTokenExpiry) {
143
- chatToken = secret;
144
- chatTokenExpiry = tokenExpiry;
166
+ // Track best candidate for each service
167
+ if (entry.target.includes('chatsvcagg.teams.microsoft.com')) {
168
+ if (!chatsvcCandidate || expiry > chatsvcCandidate.expiry) {
169
+ chatsvcCandidate = { token: entry.secret, expiry };
145
170
  }
146
171
  }
147
- // Fallback to api.spaces.skype.com token
148
- if (val.target.includes('api.spaces.skype.com')) {
149
- if (!skypeTokenExpiry || tokenExpiry > skypeTokenExpiry) {
150
- skypeToken = secret;
151
- skypeTokenExpiry = tokenExpiry;
172
+ else if (entry.target.includes('api.spaces.skype.com')) {
173
+ if (!skypeCandidate || expiry > skypeCandidate.expiry) {
174
+ skypeCandidate = { token: entry.secret, expiry };
152
175
  }
153
176
  }
154
177
  }
@@ -156,107 +179,108 @@ export function extractTeamsToken(state) {
156
179
  continue;
157
180
  }
158
181
  }
159
- // If we still don't have userMri, try to get it from the Substrate token
182
+ // Fallback: extract userMri from Substrate token if not found
160
183
  if (!userMri) {
161
- const substrateInfo = extractSubstrateToken(sessionState);
162
- if (substrateInfo) {
163
- const payload = decodeJwtPayload(substrateInfo.token);
164
- if (typeof payload?.oid === 'string') {
165
- userMri = `8:orgid:${payload.oid}`;
166
- }
167
- }
184
+ userMri = extractUserMriFromSubstrate(state);
168
185
  }
169
- // Prefer chatsvc token, fallback to skype token
170
- const token = chatToken || skypeToken;
171
- const expiry = chatToken ? chatTokenExpiry : skypeTokenExpiry;
172
- if (token && expiry && userMri && expiry.getTime() > Date.now()) {
173
- return { token, expiry, userMri };
186
+ // Prefer chatsvc, fall back to skype
187
+ const best = chatsvcCandidate ?? skypeCandidate;
188
+ if (!best || !userMri || best.expiry.getTime() <= Date.now()) {
189
+ return null;
190
+ }
191
+ return { token: best.token, expiry: best.expiry, userMri };
192
+ }
193
+ /**
194
+ * Extracts user MRI from the Substrate token's oid claim.
195
+ */
196
+ function extractUserMriFromSubstrate(state) {
197
+ const substrateInfo = extractSubstrateToken(state);
198
+ if (!substrateInfo)
199
+ return null;
200
+ const payload = decodeJwtPayload(substrateInfo.token);
201
+ if (typeof payload?.oid === 'string') {
202
+ return `${MRI_ORGID_PREFIX}${payload.oid}`;
174
203
  }
175
204
  return null;
176
205
  }
177
206
  /**
178
- * Extracts authentication info needed for messaging API (uses cookies).
207
+ * Extracts authentication info needed for messaging API.
208
+ * Unlike other APIs, messaging uses cookies rather than localStorage tokens.
179
209
  */
180
210
  export function extractMessageAuth(state) {
181
211
  const sessionState = state ?? readSessionState();
182
212
  if (!sessionState)
183
213
  return null;
184
- let skypeToken = null;
185
- let authToken = null;
186
- let userMri = null;
187
- // Extract tokens from cookies
188
- for (const cookie of sessionState.cookies || []) {
189
- if (cookie.name === 'skypetoken_asm' && cookie.domain?.includes('teams.microsoft.com')) {
190
- skypeToken = cookie.value;
191
- }
192
- if (cookie.name === 'authtoken' && cookie.domain?.includes('teams.microsoft.com')) {
193
- authToken = decodeURIComponent(cookie.value);
194
- if (authToken.startsWith('Bearer=')) {
195
- authToken = authToken.substring(7);
196
- }
197
- }
198
- }
199
- // Get userMri from skypeToken payload
200
- if (skypeToken) {
201
- const payload = decodeJwtPayload(skypeToken);
202
- if (typeof payload?.skypeid === 'string') {
203
- userMri = payload.skypeid;
204
- }
205
- }
206
- // Fallback to extracting userMri from authToken
207
- if (!userMri && authToken) {
208
- const payload = decodeJwtPayload(authToken);
209
- if (typeof payload?.oid === 'string') {
210
- userMri = `8:orgid:${payload.oid}`;
211
- }
212
- }
213
- if (skypeToken && authToken && userMri) {
214
- return { skypeToken, authToken, userMri };
214
+ const cookies = sessionState.cookies ?? [];
215
+ const teamsCookies = cookies.filter(c => c.domain?.includes('teams.microsoft.com'));
216
+ // Extract the two required cookies
217
+ const skypeToken = teamsCookies.find(c => c.name === 'skypetoken_asm')?.value ?? null;
218
+ const rawAuthToken = teamsCookies.find(c => c.name === 'authtoken')?.value ?? null;
219
+ if (!skypeToken || !rawAuthToken)
220
+ return null;
221
+ // Decode authtoken (URL-encoded, may have 'Bearer=' prefix)
222
+ let authToken = decodeURIComponent(rawAuthToken);
223
+ if (authToken.startsWith('Bearer=')) {
224
+ authToken = authToken.substring(7);
215
225
  }
216
- return null;
226
+ // Extract userMri from skypeToken's skypeid claim, or fall back to authToken's oid
227
+ const userMri = extractMriFromSkypeToken(skypeToken)
228
+ ?? extractMriFromAuthToken(authToken);
229
+ if (!userMri)
230
+ return null;
231
+ return { skypeToken, authToken, userMri };
232
+ }
233
+ function extractMriFromSkypeToken(token) {
234
+ const payload = decodeJwtPayload(token);
235
+ return typeof payload?.skypeid === 'string' ? payload.skypeid : null;
236
+ }
237
+ function extractMriFromAuthToken(token) {
238
+ const payload = decodeJwtPayload(token);
239
+ return typeof payload?.oid === 'string' ? `${MRI_ORGID_PREFIX}${payload.oid}` : null;
217
240
  }
218
241
  /**
219
242
  * Extracts the CSA token for the conversationFolders API.
243
+ * This searches all origins, not just teams.microsoft.com.
220
244
  */
221
245
  export function extractCsaToken(state) {
222
246
  const sessionState = state ?? readSessionState();
223
247
  if (!sessionState)
224
248
  return null;
225
- for (const origin of sessionState.origins || []) {
226
- for (const item of origin.localStorage || []) {
227
- if (item.name.includes('chatsvcagg.teams.microsoft.com') && !item.name.startsWith('tmp.')) {
228
- try {
229
- const data = JSON.parse(item.value);
230
- if (data.secret) {
231
- return data.secret;
232
- }
233
- }
234
- catch {
235
- // Ignore parse errors
236
- }
249
+ for (const origin of sessionState.origins ?? []) {
250
+ for (const item of origin.localStorage ?? []) {
251
+ // Skip temporary entries, look for chatsvcagg tokens
252
+ if (item.name.startsWith('tmp.'))
253
+ continue;
254
+ if (!item.name.includes('chatsvcagg.teams.microsoft.com'))
255
+ continue;
256
+ try {
257
+ const entry = JSON.parse(item.value);
258
+ if (entry.secret)
259
+ return entry.secret;
260
+ }
261
+ catch {
262
+ // Ignore parse errors
237
263
  }
238
264
  }
239
265
  }
240
266
  return null;
241
267
  }
268
+ // ============================================================================
269
+ // User Profile
270
+ // ============================================================================
242
271
  /**
243
272
  * Gets the current user's profile from cached JWT tokens.
244
273
  */
245
274
  export function getUserProfile(state) {
246
- const sessionState = state ?? readSessionState();
247
- if (!sessionState)
248
- return null;
249
- const teamsOrigin = getTeamsOrigin(sessionState);
250
- if (!teamsOrigin)
275
+ const localStorage = getTeamsLocalStorage(state);
276
+ if (!localStorage)
251
277
  return null;
252
- for (const item of teamsOrigin.localStorage) {
278
+ for (const item of localStorage) {
253
279
  try {
254
- const val = JSON.parse(item.value);
255
- if (!val.secret || typeof val.secret !== 'string')
256
- continue;
257
- if (!val.secret.startsWith('ey'))
280
+ const entry = JSON.parse(item.value);
281
+ if (!isJwtToken(entry.secret))
258
282
  continue;
259
- const payload = decodeJwtPayload(val.secret);
283
+ const payload = decodeJwtPayload(entry.secret);
260
284
  if (payload) {
261
285
  const profile = parseJwtProfile(payload);
262
286
  if (profile)
@@ -271,30 +295,31 @@ export function getUserProfile(state) {
271
295
  }
272
296
  /**
273
297
  * Gets user's display name from session state.
298
+ * Searches localStorage entries first, then falls back to JWT claims.
274
299
  */
275
300
  export function getUserDisplayName(state) {
276
- const sessionState = state ?? readSessionState();
277
- if (!sessionState)
278
- return null;
279
- const teamsOrigin = getTeamsOrigin(sessionState);
280
- if (!teamsOrigin)
301
+ const localStorage = getTeamsLocalStorage(state);
302
+ if (!localStorage)
281
303
  return null;
282
- for (const item of teamsOrigin.localStorage) {
304
+ // First pass: look for explicit displayName in localStorage
305
+ for (const item of localStorage) {
306
+ // Quick filter before parsing
307
+ if (!item.value?.includes('displayName') && !item.value?.includes('givenName')) {
308
+ continue;
309
+ }
283
310
  try {
284
- if (item.value?.includes('displayName') || item.value?.includes('givenName')) {
285
- const val = JSON.parse(item.value);
286
- if (val.displayName)
287
- return val.displayName;
288
- if (val.name?.displayName)
289
- return val.name.displayName;
290
- }
311
+ const entry = JSON.parse(item.value);
312
+ if (entry.displayName)
313
+ return entry.displayName;
314
+ if (entry.name?.displayName)
315
+ return entry.name.displayName;
291
316
  }
292
317
  catch {
293
318
  continue;
294
319
  }
295
320
  }
296
- // Try to get from token
297
- const teamsToken = extractTeamsToken(sessionState);
321
+ // Fallback: extract from Teams token's name claim
322
+ const teamsToken = extractTeamsToken(state);
298
323
  if (teamsToken) {
299
324
  const payload = decodeJwtPayload(teamsToken.token);
300
325
  if (typeof payload?.name === 'string')
@@ -302,6 +327,9 @@ export function getUserDisplayName(state) {
302
327
  }
303
328
  return null;
304
329
  }
330
+ // ============================================================================
331
+ // Token Status Checks
332
+ // ============================================================================
305
333
  /**
306
334
  * Checks if tokens in session state are expired.
307
335
  */
@@ -23,25 +23,3 @@ export interface TokenRefreshResult {
23
23
  * trigger that and save the updated state.
24
24
  */
25
25
  export declare function refreshTokensViaBrowser(): Promise<Result<TokenRefreshResult>>;
26
- /**
27
- * Checks if tokens need refreshing (less than threshold time remaining).
28
- *
29
- * @returns true if tokens should be refreshed proactively
30
- */
31
- export declare function shouldRefreshToken(): boolean;
32
- /**
33
- * Checks if tokens are completely expired.
34
- *
35
- * @returns true if tokens have expired and cannot be used
36
- */
37
- export declare function isTokenExpired(): boolean;
38
- /**
39
- * Attempts to proactively refresh tokens if they're approaching expiry.
40
- *
41
- * This is a convenience function that checks if refresh is needed and
42
- * performs it if necessary. Safe to call on any operation - it only
43
- * opens a browser when actually needed.
44
- *
45
- * @returns The refresh result if refresh was attempted, null if not needed
46
- */
47
- export declare function refreshIfNeeded(): Promise<Result<TokenRefreshResult> | null>;
@@ -5,7 +5,7 @@
5
5
  * requests. We open a headless browser with saved session state, let MSAL
6
6
  * silently refresh tokens, then save the updated state. Seamless to the user.
7
7
  */
8
- import { TOKEN_REFRESH_THRESHOLD_MS, MSAL_TOKEN_DELAY_MS } from '../constants.js';
8
+ import { MSAL_TOKEN_DELAY_MS, TOKEN_REFRESH_THRESHOLD_MS } from '../constants.js';
9
9
  import { ErrorCode, createError } from '../types/errors.js';
10
10
  import { ok, err } from '../types/result.js';
11
11
  import { extractSubstrateToken, clearTokenCache, } from './token-extractor.js';
@@ -81,41 +81,3 @@ export async function refreshTokensViaBrowser() {
81
81
  return err(createError(ErrorCode.UNKNOWN, `Token refresh via browser failed: ${message}`, { suggestions: ['Call teams_login to re-authenticate'] }));
82
82
  }
83
83
  }
84
- /**
85
- * Checks if tokens need refreshing (less than threshold time remaining).
86
- *
87
- * @returns true if tokens should be refreshed proactively
88
- */
89
- export function shouldRefreshToken() {
90
- const substrate = extractSubstrateToken();
91
- if (!substrate)
92
- return false;
93
- const timeRemaining = substrate.expiry.getTime() - Date.now();
94
- return timeRemaining > 0 && timeRemaining < TOKEN_REFRESH_THRESHOLD_MS;
95
- }
96
- /**
97
- * Checks if tokens are completely expired.
98
- *
99
- * @returns true if tokens have expired and cannot be used
100
- */
101
- export function isTokenExpired() {
102
- const substrate = extractSubstrateToken();
103
- if (!substrate)
104
- return true;
105
- return substrate.expiry.getTime() <= Date.now();
106
- }
107
- /**
108
- * Attempts to proactively refresh tokens if they're approaching expiry.
109
- *
110
- * This is a convenience function that checks if refresh is needed and
111
- * performs it if necessary. Safe to call on any operation - it only
112
- * opens a browser when actually needed.
113
- *
114
- * @returns The refresh result if refresh was attempted, null if not needed
115
- */
116
- export async function refreshIfNeeded() {
117
- if (!shouldRefreshToken()) {
118
- return null;
119
- }
120
- return refreshTokensViaBrowser();
121
- }
@@ -62,3 +62,5 @@ export declare const MAX_ACTIVITY_LIMIT = 200;
62
62
  export declare const MAX_UNREAD_AGGREGATE_CHECK = 20;
63
63
  /** Threshold for proactive token refresh (10 minutes before expiry). */
64
64
  export declare const TOKEN_REFRESH_THRESHOLD_MS: number;
65
+ /** MRI prefix for organisation users (orgid). */
66
+ export declare const MRI_ORGID_PREFIX = "8:orgid:";
package/dist/constants.js CHANGED
@@ -89,3 +89,8 @@ export const MAX_UNREAD_AGGREGATE_CHECK = 20;
89
89
  // ─────────────────────────────────────────────────────────────────────────────
90
90
  /** Threshold for proactive token refresh (10 minutes before expiry). */
91
91
  export const TOKEN_REFRESH_THRESHOLD_MS = 10 * 60 * 1000;
92
+ // ─────────────────────────────────────────────────────────────────────────────
93
+ // User Identity
94
+ // ─────────────────────────────────────────────────────────────────────────────
95
+ /** MRI prefix for organisation users (orgid). */
96
+ export const MRI_ORGID_PREFIX = '8:orgid:';
@@ -28,13 +28,13 @@ export const FindChannelInputSchema = z.object({
28
28
  // ─────────────────────────────────────────────────────────────────────────────
29
29
  const searchToolDefinition = {
30
30
  name: 'teams_search',
31
- description: 'Search for messages in Microsoft Teams. Returns matching messages with sender, timestamp, content, conversationId (for replies), and pagination info. Supports search operators: from:email, sent:today/lastweek, in:channel, hasattachment:true, "Name" for @mentions. Combine with NOT to exclude (e.g., NOT from:rob@co.com).',
31
+ description: 'Search for messages in Microsoft Teams. Returns matching messages with sender, timestamp, content, conversationId (for replies), and pagination info. Supports search operators: from:email, sent:YYYY-MM-DD, in:channel, hasattachment:true, "Name" for @mentions. Combine with NOT to exclude (e.g., NOT from:rob@co.com). Results are sorted by recency.',
32
32
  inputSchema: {
33
33
  type: 'object',
34
34
  properties: {
35
35
  query: {
36
36
  type: 'string',
37
- description: 'Search query with optional operators. Examples: "budget report", "from:sarah@co.com sent:lastweek", "\"Rob Smith\" NOT from:rob@co.com" (find @mentions of Rob). IMPORTANT: "@me", "from:me", "to:me" do NOT work - use teams_get_me first to get actual email/displayName, then use those values.',
37
+ description: 'Search query with optional operators. Examples: "budget report", "from:sarah@co.com", "\"Rob Smith\" NOT from:rob@co.com" (find @mentions of Rob). For date filtering use sent:YYYY-MM-DD or sent:>=YYYY-MM-DD (e.g., sent:>=2026-01-20). IMPORTANT: "@me", "from:me", "to:me" do NOT work - use teams_get_me first to get actual email/displayName. Also "sent:lastweek" and "sent:today" do NOT work - use explicit dates or omit (results are sorted by recency).',
38
38
  },
39
39
  maxResults: {
40
40
  type: 'number',
@@ -12,11 +12,6 @@ export interface CsaAuthInfo {
12
12
  auth: MessageAuthInfo;
13
13
  csaToken: string;
14
14
  }
15
- /**
16
- * Requires a valid Substrate token.
17
- * Use for search and people APIs.
18
- */
19
- export declare function requireSubstrateToken(): Result<string, McpError>;
20
15
  /**
21
16
  * Requires a valid Substrate token with proactive refresh.
22
17
  *
@@ -13,24 +13,12 @@ import { refreshTokensViaBrowser } from '../auth/token-refresh.js';
13
13
  // Error Messages
14
14
  // ─────────────────────────────────────────────────────────────────────────────
15
15
  const AUTH_ERROR_MESSAGES = {
16
- substrateToken: 'No valid token available. Browser login required.',
17
16
  messageAuth: 'No valid authentication. Browser login required.',
18
17
  csaToken: 'No valid authentication for favourites. Browser login required.',
19
18
  };
20
19
  // ─────────────────────────────────────────────────────────────────────────────
21
20
  // Guard Functions
22
21
  // ─────────────────────────────────────────────────────────────────────────────
23
- /**
24
- * Requires a valid Substrate token.
25
- * Use for search and people APIs.
26
- */
27
- export function requireSubstrateToken() {
28
- const token = getValidSubstrateToken();
29
- if (!token) {
30
- return err(createError(ErrorCode.AUTH_REQUIRED, AUTH_ERROR_MESSAGES.substrateToken));
31
- }
32
- return ok(token);
33
- }
34
22
  /**
35
23
  * Checks if the Substrate token is approaching expiry and needs refresh.
36
24
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "msteams-mcp",
3
- "version": "0.3.3",
3
+ "version": "0.3.5",
4
4
  "description": "MCP server for Microsoft Teams - search messages, send replies, manage favourites",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",