@softeria/ms-365-mcp-server 0.113.0 → 0.114.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.
- package/dist/endpoints.json +0 -6
- package/dist/generated/client.js +54 -1
- package/dist/graph-client.js +16 -6
- package/dist/lib/graph-resilience.js +208 -0
- package/docs/deployment.md +1 -0
- package/package.json +3 -3
- package/src/endpoints.json +0 -6
package/dist/endpoints.json
CHANGED
|
@@ -489,12 +489,6 @@
|
|
|
489
489
|
"toolName": "get-drive-root-item",
|
|
490
490
|
"scopes": ["Files.Read"]
|
|
491
491
|
},
|
|
492
|
-
{
|
|
493
|
-
"pathPattern": "/drives/{drive-id}/root",
|
|
494
|
-
"method": "get",
|
|
495
|
-
"toolName": "get-drive-root-item",
|
|
496
|
-
"scopes": ["Files.Read"]
|
|
497
|
-
},
|
|
498
492
|
{
|
|
499
493
|
"pathPattern": "/drives/{drive-id}/items/{driveItem-id}/children",
|
|
500
494
|
"method": "get",
|
package/dist/generated/client.js
CHANGED
|
@@ -365,6 +365,57 @@ const microsoft_graph_teamsTab = z.object({
|
|
|
365
365
|
webUrl: z.string().describe("Deep link URL of the tab instance. Read-only.").nullish(),
|
|
366
366
|
teamsApp: microsoft_graph_teamsApp.optional()
|
|
367
367
|
}).passthrough();
|
|
368
|
+
const microsoft_graph_targetedChatMessage = z.object({
|
|
369
|
+
id: z.string().describe("The unique identifier for an entity. Read-only.").optional(),
|
|
370
|
+
createdDateTime: z.string().regex(
|
|
371
|
+
/^[0-9]{4,}-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])T([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9]([.][0-9]{1,12})?(Z|[+-][0-9][0-9]:[0-9][0-9])$/
|
|
372
|
+
).datetime({ offset: true }).describe("Timestamp of when the chat message was created.").nullish(),
|
|
373
|
+
lastModifiedDateTime: z.string().regex(
|
|
374
|
+
/^[0-9]{4,}-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])T([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9]([.][0-9]{1,12})?(Z|[+-][0-9][0-9]:[0-9][0-9])$/
|
|
375
|
+
).datetime({ offset: true }).describe(
|
|
376
|
+
"Read only. Timestamp when the chat message is created (initial setting) or modified, including when a reaction is added or removed."
|
|
377
|
+
).nullish(),
|
|
378
|
+
body: microsoft_graph_itemBody.optional(),
|
|
379
|
+
subject: z.string().describe("The subject of the chat message, in plaintext.").nullish(),
|
|
380
|
+
attachments: z.array(microsoft_graph_chatMessageAttachment).describe("References to attached objects like files, tabs, meetings etc.").optional(),
|
|
381
|
+
importance: microsoft_graph_chatMessageImportance.optional(),
|
|
382
|
+
from: microsoft_graph_chatMessageFromIdentitySet.optional(),
|
|
383
|
+
channelIdentity: microsoft_graph_channelIdentity.optional(),
|
|
384
|
+
chatId: z.string().describe("If the message was sent in a chat, represents the identity of the chat.").nullish(),
|
|
385
|
+
deletedDateTime: z.string().regex(
|
|
386
|
+
/^[0-9]{4,}-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])T([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9]([.][0-9]{1,12})?(Z|[+-][0-9][0-9]:[0-9][0-9])$/
|
|
387
|
+
).datetime({ offset: true }).describe(
|
|
388
|
+
"Read only. Timestamp at which the chat message was deleted, or null if not deleted."
|
|
389
|
+
).nullish(),
|
|
390
|
+
etag: z.string().describe("Read-only. Version number of the chat message.").nullish(),
|
|
391
|
+
eventDetail: microsoft_graph_eventMessageDetail.optional(),
|
|
392
|
+
lastEditedDateTime: z.string().regex(
|
|
393
|
+
/^[0-9]{4,}-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])T([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9]([.][0-9]{1,12})?(Z|[+-][0-9][0-9]:[0-9][0-9])$/
|
|
394
|
+
).datetime({ offset: true }).describe(
|
|
395
|
+
"Read only. Timestamp when edits to the chat message were made. Triggers an 'Edited' flag in the Teams UI. If no edits are made the value is null."
|
|
396
|
+
).nullish(),
|
|
397
|
+
locale: z.string().describe("Locale of the chat message set by the client. Always set to en-us.").optional(),
|
|
398
|
+
mentions: z.array(microsoft_graph_chatMessageMention).describe(
|
|
399
|
+
"List of entities mentioned in the chat message. Supported entities are: user, bot, team, channel, chat, and tag."
|
|
400
|
+
).optional(),
|
|
401
|
+
messageHistory: z.array(microsoft_graph_chatMessageHistoryItem).describe(
|
|
402
|
+
"List of activity history of a message item, including modification time and actions, such as reactionAdded, reactionRemoved, or reaction changes, on the message."
|
|
403
|
+
).optional(),
|
|
404
|
+
messageType: microsoft_graph_chatMessageType.optional(),
|
|
405
|
+
policyViolation: microsoft_graph_chatMessagePolicyViolation.optional(),
|
|
406
|
+
reactions: z.array(microsoft_graph_chatMessageReaction).describe("Reactions for this chat message (for example, Like).").optional(),
|
|
407
|
+
replyToId: z.string().describe(
|
|
408
|
+
"Read-only. ID of the parent chat message or root chat message of the thread. (Only applies to chat messages in channels, not chats.)"
|
|
409
|
+
).nullish(),
|
|
410
|
+
summary: z.string().describe(
|
|
411
|
+
"Summary text of the chat message that could be used for push notifications and summary views or fall back views. Only applies to channel chat messages, not chat messages in a chat."
|
|
412
|
+
).nullish(),
|
|
413
|
+
webUrl: z.string().describe("Read-only. Link to the message in Microsoft Teams.").nullish(),
|
|
414
|
+
hostedContents: z.array(microsoft_graph_chatMessageHostedContent).describe(
|
|
415
|
+
"Content in a message hosted by Microsoft Teams - for example, images or code snippets."
|
|
416
|
+
).optional(),
|
|
417
|
+
replies: z.array(microsoft_graph_chatMessage).describe("Replies for a specified message. Supports $expand for channel messages.").optional()
|
|
418
|
+
}).passthrough().passthrough();
|
|
368
419
|
const microsoft_graph_chat = z.object({
|
|
369
420
|
id: z.string().describe("The unique identifier for an entity. Read-only.").optional(),
|
|
370
421
|
chatType: microsoft_graph_chatType.optional(),
|
|
@@ -396,7 +447,8 @@ const microsoft_graph_chat = z.object({
|
|
|
396
447
|
messages: z.array(microsoft_graph_chatMessage).describe("A collection of all the messages in the chat. Nullable.").optional(),
|
|
397
448
|
permissionGrants: z.array(microsoft_graph_resourceSpecificPermissionGrant).describe("A collection of permissions granted to apps for the chat.").optional(),
|
|
398
449
|
pinnedMessages: z.array(microsoft_graph_pinnedChatMessageInfo).describe("A collection of all the pinned messages in the chat. Nullable.").optional(),
|
|
399
|
-
tabs: z.array(microsoft_graph_teamsTab).describe("A collection of all the tabs in the chat. Nullable.").optional()
|
|
450
|
+
tabs: z.array(microsoft_graph_teamsTab).describe("A collection of all the tabs in the chat. Nullable.").optional(),
|
|
451
|
+
targetedMessages: z.array(microsoft_graph_targetedChatMessage).optional()
|
|
400
452
|
}).passthrough();
|
|
401
453
|
const microsoft_graph_ODataErrors_ErrorDetails = z.object({ code: z.string(), message: z.string(), target: z.string().nullish() }).passthrough();
|
|
402
454
|
const microsoft_graph_ODataErrors_InnerError = z.object({
|
|
@@ -4666,6 +4718,7 @@ const schemas = {
|
|
|
4666
4718
|
microsoft_graph_pinnedChatMessageInfo,
|
|
4667
4719
|
microsoft_graph_teamsTabConfiguration,
|
|
4668
4720
|
microsoft_graph_teamsTab,
|
|
4721
|
+
microsoft_graph_targetedChatMessage,
|
|
4669
4722
|
microsoft_graph_chat,
|
|
4670
4723
|
microsoft_graph_ODataErrors_ErrorDetails,
|
|
4671
4724
|
microsoft_graph_ODataErrors_InnerError,
|
package/dist/graph-client.js
CHANGED
|
@@ -2,6 +2,11 @@ import logger from "./logger.js";
|
|
|
2
2
|
import { encode as toonEncode } from "@toon-format/toon";
|
|
3
3
|
import { getCloudEndpoints } from "./cloud-config.js";
|
|
4
4
|
import { getRequestTokens } from "./request-context.js";
|
|
5
|
+
import {
|
|
6
|
+
fetchWithResilience,
|
|
7
|
+
getSharedBreaker,
|
|
8
|
+
loadResilienceConfig
|
|
9
|
+
} from "./lib/graph-resilience.js";
|
|
5
10
|
function isBinaryContentType(contentType) {
|
|
6
11
|
if (!contentType) return false;
|
|
7
12
|
const lower = contentType.toLowerCase().split(";")[0].trim();
|
|
@@ -102,12 +107,17 @@ class GraphClient {
|
|
|
102
107
|
"Content-Type": "application/json",
|
|
103
108
|
...options.headers
|
|
104
109
|
};
|
|
105
|
-
return
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
110
|
+
return fetchWithResilience(
|
|
111
|
+
url,
|
|
112
|
+
{
|
|
113
|
+
method: options.method || "GET",
|
|
114
|
+
headers,
|
|
115
|
+
// Node's fetch accepts Buffer/Uint8Array; TS BodyInit doesn't.
|
|
116
|
+
body: options.body
|
|
117
|
+
},
|
|
118
|
+
loadResilienceConfig(),
|
|
119
|
+
getSharedBreaker()
|
|
120
|
+
);
|
|
111
121
|
}
|
|
112
122
|
serializeData(data, outputFormat, pretty = false) {
|
|
113
123
|
if (outputFormat === "toon") {
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import logger from "../logger.js";
|
|
2
|
+
function loadResilienceConfig() {
|
|
3
|
+
const intEnv = (name, fallback) => {
|
|
4
|
+
const raw = process.env[name];
|
|
5
|
+
if (raw === void 0 || raw === "") return fallback;
|
|
6
|
+
const n = Number.parseInt(raw, 10);
|
|
7
|
+
if (!Number.isFinite(n) || n < 0) {
|
|
8
|
+
logger.warn(`Ignoring invalid ${name}=${JSON.stringify(raw)} (use a non-negative integer)`);
|
|
9
|
+
return fallback;
|
|
10
|
+
}
|
|
11
|
+
return n;
|
|
12
|
+
};
|
|
13
|
+
return {
|
|
14
|
+
maxRetries: intEnv("MS365_MCP_GRAPH_MAX_RETRIES", 3),
|
|
15
|
+
baseBackoffMs: intEnv("MS365_MCP_GRAPH_BASE_BACKOFF_MS", 200),
|
|
16
|
+
maxBackoffMs: intEnv("MS365_MCP_GRAPH_MAX_BACKOFF_MS", 5e3),
|
|
17
|
+
fetchTimeoutMs: intEnv("MS365_MCP_GRAPH_TIMEOUT_MS", 1e5),
|
|
18
|
+
circuitFailureThreshold: intEnv("MS365_MCP_GRAPH_CIRCUIT_THRESHOLD", 5),
|
|
19
|
+
circuitCooldownMs: intEnv("MS365_MCP_GRAPH_CIRCUIT_COOLDOWN_MS", 3e4),
|
|
20
|
+
circuitDisabled: process.env.MS365_MCP_GRAPH_CIRCUIT_DISABLED === "true" || process.env.MS365_MCP_GRAPH_CIRCUIT_DISABLED === "1"
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
class CircuitOpenError extends Error {
|
|
24
|
+
constructor(cooldownMs) {
|
|
25
|
+
super(
|
|
26
|
+
`Graph circuit breaker is open (cooldown ${cooldownMs} ms). Upstream has failed repeatedly; refusing to flood it.`
|
|
27
|
+
);
|
|
28
|
+
this.code = "circuit_open";
|
|
29
|
+
this.name = "CircuitOpenError";
|
|
30
|
+
this.cooldownMs = cooldownMs;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
class CircuitBreaker {
|
|
34
|
+
constructor(threshold, cooldownMs, disabled, now = () => Date.now()) {
|
|
35
|
+
this.threshold = threshold;
|
|
36
|
+
this.cooldownMs = cooldownMs;
|
|
37
|
+
this.disabled = disabled;
|
|
38
|
+
this.now = now;
|
|
39
|
+
this.failures = 0;
|
|
40
|
+
this.openedAt = null;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* @returns the time-remaining (in ms) before the circuit can be probed,
|
|
44
|
+
* or `null` if the circuit is closed and the call should proceed.
|
|
45
|
+
*/
|
|
46
|
+
checkBeforeRequest() {
|
|
47
|
+
if (this.disabled) return null;
|
|
48
|
+
if (this.openedAt === null) return null;
|
|
49
|
+
const elapsed = this.now() - this.openedAt;
|
|
50
|
+
if (elapsed >= this.cooldownMs) {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
return this.cooldownMs - elapsed;
|
|
54
|
+
}
|
|
55
|
+
recordSuccess() {
|
|
56
|
+
if (this.failures !== 0 || this.openedAt !== null) {
|
|
57
|
+
logger.info("Graph circuit: success \u2014 closing breaker");
|
|
58
|
+
}
|
|
59
|
+
this.failures = 0;
|
|
60
|
+
this.openedAt = null;
|
|
61
|
+
}
|
|
62
|
+
recordFailure() {
|
|
63
|
+
if (this.disabled) return;
|
|
64
|
+
this.failures += 1;
|
|
65
|
+
if (this.failures >= this.threshold && this.openedAt === null) {
|
|
66
|
+
this.openedAt = this.now();
|
|
67
|
+
logger.warn(
|
|
68
|
+
`Graph circuit: ${this.failures} consecutive failures \u2014 opening breaker for ${this.cooldownMs} ms`
|
|
69
|
+
);
|
|
70
|
+
} else if (this.openedAt !== null) {
|
|
71
|
+
this.openedAt = this.now();
|
|
72
|
+
logger.warn("Graph circuit: probe failed \u2014 extending cooldown");
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/** Exposed for tests / metrics. */
|
|
76
|
+
getState() {
|
|
77
|
+
return {
|
|
78
|
+
failures: this.failures,
|
|
79
|
+
openedAt: this.openedAt,
|
|
80
|
+
open: this.checkBeforeRequest() !== null
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function parseRetryAfterMs(header) {
|
|
85
|
+
if (!header) return null;
|
|
86
|
+
const trimmed = header.trim();
|
|
87
|
+
if (trimmed === "") return null;
|
|
88
|
+
const asInt = Number.parseInt(trimmed, 10);
|
|
89
|
+
if (Number.isFinite(asInt) && asInt >= 0 && String(asInt) === trimmed) {
|
|
90
|
+
return Math.min(asInt * 1e3, 6e4);
|
|
91
|
+
}
|
|
92
|
+
if (!/[-/:,]| GMT$/i.test(trimmed) && !/\s+\d/.test(trimmed)) {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
const dateMs = Date.parse(trimmed);
|
|
96
|
+
if (Number.isFinite(dateMs)) {
|
|
97
|
+
const delta = dateMs - Date.now();
|
|
98
|
+
if (delta <= 0) return 0;
|
|
99
|
+
return Math.min(delta, 6e4);
|
|
100
|
+
}
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
function backoffDelayMs(attempt, baseMs, maxMs, rand = Math.random) {
|
|
104
|
+
const exp = Math.min(maxMs, baseMs * 2 ** attempt);
|
|
105
|
+
return Math.floor(rand() * exp);
|
|
106
|
+
}
|
|
107
|
+
function isRetriableStatus(status) {
|
|
108
|
+
return status === 429 || status === 503 || status === 504;
|
|
109
|
+
}
|
|
110
|
+
function isMethodIdempotent(method) {
|
|
111
|
+
const m = method.toUpperCase();
|
|
112
|
+
return m === "GET" || m === "HEAD" || m === "PUT" || m === "DELETE" || m === "OPTIONS" || m === "TRACE";
|
|
113
|
+
}
|
|
114
|
+
function isAbortError(err) {
|
|
115
|
+
return typeof err === "object" && err !== null && "name" in err && err.name === "AbortError";
|
|
116
|
+
}
|
|
117
|
+
async function fetchWithResilience(url, init, config, breaker, sleep = (ms) => new Promise((r) => setTimeout(r, ms))) {
|
|
118
|
+
const remainingCooldown = breaker.checkBeforeRequest();
|
|
119
|
+
if (remainingCooldown !== null) {
|
|
120
|
+
throw new CircuitOpenError(remainingCooldown);
|
|
121
|
+
}
|
|
122
|
+
const method = (init?.method ?? "GET").toString().toUpperCase();
|
|
123
|
+
const methodIsIdempotent = isMethodIdempotent(method);
|
|
124
|
+
let attempt = 0;
|
|
125
|
+
while (true) {
|
|
126
|
+
const controller = new AbortController();
|
|
127
|
+
const timer = setTimeout(() => controller.abort(), config.fetchTimeoutMs);
|
|
128
|
+
let response = null;
|
|
129
|
+
let networkError = null;
|
|
130
|
+
try {
|
|
131
|
+
response = await fetch(url, { ...init, signal: controller.signal });
|
|
132
|
+
} catch (err) {
|
|
133
|
+
networkError = err;
|
|
134
|
+
} finally {
|
|
135
|
+
clearTimeout(timer);
|
|
136
|
+
}
|
|
137
|
+
if (response !== null && !isRetriableStatus(response.status)) {
|
|
138
|
+
breaker.recordSuccess();
|
|
139
|
+
return response;
|
|
140
|
+
}
|
|
141
|
+
const is429 = response !== null && response.status === 429;
|
|
142
|
+
const retryAllowedByMethod = methodIsIdempotent || is429;
|
|
143
|
+
const canRetry = attempt < config.maxRetries && retryAllowedByMethod;
|
|
144
|
+
if (!canRetry) {
|
|
145
|
+
breaker.recordFailure();
|
|
146
|
+
if (response !== null) {
|
|
147
|
+
if (!retryAllowedByMethod && attempt === 0) {
|
|
148
|
+
logger.warn(
|
|
149
|
+
`Graph ${method} ${response.status}: not retried (non-idempotent method, side-effect may have landed)`
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
return response;
|
|
153
|
+
}
|
|
154
|
+
if (!retryAllowedByMethod && attempt === 0) {
|
|
155
|
+
logger.warn(
|
|
156
|
+
`Graph ${method} network error: not retried (non-idempotent method, side-effect may have landed)`
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
throw networkError ?? new Error("Graph fetch failed (unknown error)");
|
|
160
|
+
}
|
|
161
|
+
let delayMs;
|
|
162
|
+
if (response !== null && response.status === 429) {
|
|
163
|
+
const retryAfter = parseRetryAfterMs(response.headers.get("retry-after"));
|
|
164
|
+
delayMs = retryAfter !== null ? retryAfter : backoffDelayMs(attempt, config.baseBackoffMs, config.maxBackoffMs);
|
|
165
|
+
} else {
|
|
166
|
+
delayMs = backoffDelayMs(attempt, config.baseBackoffMs, config.maxBackoffMs);
|
|
167
|
+
}
|
|
168
|
+
const reason = response !== null ? `HTTP ${response.status}` : isAbortError(networkError) ? `timeout (${config.fetchTimeoutMs} ms)` : `network error: ${networkError?.message ?? "unknown"}`;
|
|
169
|
+
logger.warn(
|
|
170
|
+
`Graph retry ${attempt + 1}/${config.maxRetries} after ${reason} \u2014 sleeping ${delayMs} ms`
|
|
171
|
+
);
|
|
172
|
+
if (response !== null) {
|
|
173
|
+
try {
|
|
174
|
+
await response.arrayBuffer();
|
|
175
|
+
} catch {
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
breaker.recordFailure();
|
|
179
|
+
attempt += 1;
|
|
180
|
+
await sleep(delayMs);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
let _sharedBreaker = null;
|
|
184
|
+
function getSharedBreaker() {
|
|
185
|
+
if (_sharedBreaker === null) {
|
|
186
|
+
const cfg = loadResilienceConfig();
|
|
187
|
+
_sharedBreaker = new CircuitBreaker(
|
|
188
|
+
cfg.circuitFailureThreshold,
|
|
189
|
+
cfg.circuitCooldownMs,
|
|
190
|
+
cfg.circuitDisabled
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
return _sharedBreaker;
|
|
194
|
+
}
|
|
195
|
+
function __resetSharedBreakerForTests() {
|
|
196
|
+
_sharedBreaker = null;
|
|
197
|
+
}
|
|
198
|
+
export {
|
|
199
|
+
CircuitBreaker,
|
|
200
|
+
CircuitOpenError,
|
|
201
|
+
__resetSharedBreakerForTests,
|
|
202
|
+
backoffDelayMs,
|
|
203
|
+
fetchWithResilience,
|
|
204
|
+
getSharedBreaker,
|
|
205
|
+
isMethodIdempotent,
|
|
206
|
+
loadResilienceConfig,
|
|
207
|
+
parseRetryAfterMs
|
|
208
|
+
};
|
package/docs/deployment.md
CHANGED
|
@@ -209,6 +209,7 @@ The client automatically discovers OAuth endpoints and opens a browser for authe
|
|
|
209
209
|
- **Tool filtering**: use `--enabled-tools <regex>` or `--preset <names>` to restrict available tools
|
|
210
210
|
- **CORS**: configure `MS365_MCP_CORS_ORIGIN` to restrict allowed origins (defaults to `http://localhost:3000`); set explicitly when clients run on a different origin
|
|
211
211
|
- **Structured audit log**: enabled by default. Every tool invocation emits one JSON line on stdout (captured by the container platform's log collector) and to `~/.ms-365-mcp-server/logs/audit.log` (mode `0o600`) with `{ event, request_id, user_principal_name, tool, http_method, status, duration_ms, error_type?, error_code? }`. The schema is intentionally narrow — tool parameters and Graph response bodies are NEVER recorded, and error messages are reduced to `error_type` / `error_code` so upstream library errors do not leak token fragments or query-string PII. Forms the "who accessed what, when" trail required for GDPR / HIPAA / PIPEDA / SOC 2 audit. Opt-out: `MS365_MCP_AUDIT_LOG=false`
|
|
212
|
+
- **Graph resilience**: every call to Microsoft Graph is wrapped with a fetch timeout (default 100 s via `MS365_MCP_GRAPH_TIMEOUT_MS`), retry-with-backoff on 429 / 503 / 504 / network errors (default 3 retries, full-jitter exponential backoff, honours `Retry-After`; 503 / 504 / network errors only retried for idempotent methods, 429 retried on all methods), and a process-wide circuit breaker that opens after 5 consecutive failures and cools down for 30 s (`MS365_MCP_GRAPH_CIRCUIT_THRESHOLD` / `MS365_MCP_GRAPH_CIRCUIT_COOLDOWN_MS`). Disable the breaker for trusted automation: `MS365_MCP_GRAPH_CIRCUIT_DISABLED=true`
|
|
212
213
|
|
|
213
214
|
## Exposed Endpoints
|
|
214
215
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@softeria/ms-365-mcp-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.114.1",
|
|
4
4
|
"description": " A Model Context Protocol (MCP) server for interacting with Microsoft 365 and Office services through the Graph API",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
"@types/node": "^22.15.15",
|
|
61
61
|
"@typescript-eslint/eslint-plugin": "^8.38.0",
|
|
62
62
|
"@typescript-eslint/parser": "^8.38.0",
|
|
63
|
-
"@vitest/coverage-v8": "^
|
|
63
|
+
"@vitest/coverage-v8": "^4.1.8",
|
|
64
64
|
"eslint": "^9.31.0",
|
|
65
65
|
"globals": "^16.3.0",
|
|
66
66
|
"patch-package": "^8.0.1",
|
|
@@ -69,7 +69,7 @@
|
|
|
69
69
|
"tsup": "^8.5.0",
|
|
70
70
|
"tsx": "^4.19.4",
|
|
71
71
|
"typescript": "^5.8.3",
|
|
72
|
-
"vitest": "^
|
|
72
|
+
"vitest": "^4.1.8"
|
|
73
73
|
},
|
|
74
74
|
"engines": {
|
|
75
75
|
"node": ">=18"
|
package/src/endpoints.json
CHANGED
|
@@ -489,12 +489,6 @@
|
|
|
489
489
|
"toolName": "get-drive-root-item",
|
|
490
490
|
"scopes": ["Files.Read"]
|
|
491
491
|
},
|
|
492
|
-
{
|
|
493
|
-
"pathPattern": "/drives/{drive-id}/root",
|
|
494
|
-
"method": "get",
|
|
495
|
-
"toolName": "get-drive-root-item",
|
|
496
|
-
"scopes": ["Files.Read"]
|
|
497
|
-
},
|
|
498
492
|
{
|
|
499
493
|
"pathPattern": "/drives/{drive-id}/items/{driveItem-id}/children",
|
|
500
494
|
"method": "get",
|