@softeria/ms-365-mcp-server 0.135.0 → 0.136.0
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.
|
@@ -89,6 +89,10 @@ async function loadModule() {
|
|
|
89
89
|
const mod = await import("../graph-tools.js");
|
|
90
90
|
return mod;
|
|
91
91
|
}
|
|
92
|
+
async function spyOnAuditLogger() {
|
|
93
|
+
const { __testing } = await import("../audit-log.js");
|
|
94
|
+
return vi.spyOn(__testing.auditLogger, "info").mockImplementation(() => __testing.auditLogger);
|
|
95
|
+
}
|
|
92
96
|
function createMockServer() {
|
|
93
97
|
const tools = /* @__PURE__ */ new Map();
|
|
94
98
|
return {
|
|
@@ -139,6 +143,234 @@ describe("graph-tools", () => {
|
|
|
139
143
|
expect(url).toContain("$count=true");
|
|
140
144
|
});
|
|
141
145
|
});
|
|
146
|
+
describe("audit target resources", () => {
|
|
147
|
+
it("adds target_resource to generated Graph tool audit events", async () => {
|
|
148
|
+
const endpoint = makeEndpoint({
|
|
149
|
+
alias: "get-drive-item",
|
|
150
|
+
path: "/drives/:driveId/items/:driveItemId",
|
|
151
|
+
parameters: [
|
|
152
|
+
{ name: "driveId", type: "Path", schema: z.string() },
|
|
153
|
+
{ name: "driveItemId", type: "Path", schema: z.string() }
|
|
154
|
+
]
|
|
155
|
+
});
|
|
156
|
+
const config = makeConfig({
|
|
157
|
+
toolName: "get-drive-item",
|
|
158
|
+
pathPattern: "/drives/{drive-id}/items/{driveItem-id}",
|
|
159
|
+
scopes: ["Files.Read"]
|
|
160
|
+
});
|
|
161
|
+
mockEndpoints.push(endpoint);
|
|
162
|
+
mockEndpointsJson = [config];
|
|
163
|
+
const graphClient = createMockGraphClient([
|
|
164
|
+
{ content: [{ type: "text", text: JSON.stringify({ id: "item-2" }) }] }
|
|
165
|
+
]);
|
|
166
|
+
const server = createMockServer();
|
|
167
|
+
const { registerGraphTools } = await loadModule();
|
|
168
|
+
registerGraphTools(server, graphClient);
|
|
169
|
+
const auditSpy = await spyOnAuditLogger();
|
|
170
|
+
await server.tools.get("get-drive-item").handler({
|
|
171
|
+
driveId: "drive-1",
|
|
172
|
+
driveItemId: "item-2"
|
|
173
|
+
});
|
|
174
|
+
expect(auditSpy).toHaveBeenCalledWith(
|
|
175
|
+
expect.objectContaining({
|
|
176
|
+
event: "tool.call",
|
|
177
|
+
tool: "get-drive-item",
|
|
178
|
+
status: "success",
|
|
179
|
+
target_resource: {
|
|
180
|
+
type: "drive_item",
|
|
181
|
+
id: "/drives/drive-1/items/item-2"
|
|
182
|
+
}
|
|
183
|
+
})
|
|
184
|
+
);
|
|
185
|
+
auditSpy.mockRestore();
|
|
186
|
+
});
|
|
187
|
+
it("adds target_resource to failed generated Graph tool audit events", async () => {
|
|
188
|
+
const endpoint = makeEndpoint({
|
|
189
|
+
alias: "get-drive-item",
|
|
190
|
+
path: "/drives/:driveId/items/:driveItemId",
|
|
191
|
+
parameters: [
|
|
192
|
+
{ name: "driveId", type: "Path", schema: z.string() },
|
|
193
|
+
{ name: "driveItemId", type: "Path", schema: z.string() }
|
|
194
|
+
]
|
|
195
|
+
});
|
|
196
|
+
const config = makeConfig({
|
|
197
|
+
toolName: "get-drive-item",
|
|
198
|
+
pathPattern: "/drives/{drive-id}/items/{driveItem-id}",
|
|
199
|
+
scopes: ["Files.Read"]
|
|
200
|
+
});
|
|
201
|
+
mockEndpoints.push(endpoint);
|
|
202
|
+
mockEndpointsJson = [config];
|
|
203
|
+
const graphClient = createMockGraphClient();
|
|
204
|
+
graphClient.graphRequest.mockRejectedValueOnce(
|
|
205
|
+
Object.assign(new Error("Forbidden"), { status: 403 })
|
|
206
|
+
);
|
|
207
|
+
const server = createMockServer();
|
|
208
|
+
const { registerGraphTools } = await loadModule();
|
|
209
|
+
registerGraphTools(
|
|
210
|
+
server,
|
|
211
|
+
graphClient
|
|
212
|
+
);
|
|
213
|
+
const auditSpy = await spyOnAuditLogger();
|
|
214
|
+
const result = await server.tools.get("get-drive-item").handler({
|
|
215
|
+
driveId: "drive-1",
|
|
216
|
+
driveItemId: "item-2"
|
|
217
|
+
});
|
|
218
|
+
expect(result.isError).toBe(true);
|
|
219
|
+
expect(auditSpy).toHaveBeenCalledWith(
|
|
220
|
+
expect.objectContaining({
|
|
221
|
+
event: "tool.call",
|
|
222
|
+
tool: "get-drive-item",
|
|
223
|
+
status: "error",
|
|
224
|
+
error_code: 403,
|
|
225
|
+
target_resource: {
|
|
226
|
+
type: "drive_item",
|
|
227
|
+
id: "/drives/drive-1/items/item-2"
|
|
228
|
+
}
|
|
229
|
+
})
|
|
230
|
+
);
|
|
231
|
+
auditSpy.mockRestore();
|
|
232
|
+
});
|
|
233
|
+
it("derives target_resource from generic ID path parameters", async () => {
|
|
234
|
+
const endpoint = makeEndpoint({
|
|
235
|
+
alias: "get-mail-message",
|
|
236
|
+
path: "/me/messages/:messageId",
|
|
237
|
+
parameters: [{ name: "messageId", type: "Path", schema: z.string() }]
|
|
238
|
+
});
|
|
239
|
+
const config = makeConfig({
|
|
240
|
+
toolName: "get-mail-message",
|
|
241
|
+
pathPattern: "/me/messages/{message-id}"
|
|
242
|
+
});
|
|
243
|
+
mockEndpoints.push(endpoint);
|
|
244
|
+
mockEndpointsJson = [config];
|
|
245
|
+
const graphClient = createMockGraphClient([
|
|
246
|
+
{ content: [{ type: "text", text: JSON.stringify({ id: "message-1" }) }] }
|
|
247
|
+
]);
|
|
248
|
+
const server = createMockServer();
|
|
249
|
+
const { registerGraphTools } = await loadModule();
|
|
250
|
+
registerGraphTools(server, graphClient);
|
|
251
|
+
const auditSpy = await spyOnAuditLogger();
|
|
252
|
+
await server.tools.get("get-mail-message").handler({
|
|
253
|
+
messageId: "message-1"
|
|
254
|
+
});
|
|
255
|
+
expect(auditSpy).toHaveBeenCalledWith(
|
|
256
|
+
expect.objectContaining({
|
|
257
|
+
event: "tool.call",
|
|
258
|
+
tool: "get-mail-message",
|
|
259
|
+
status: "success",
|
|
260
|
+
target_resource: {
|
|
261
|
+
type: "message",
|
|
262
|
+
id: "/me/messages/message-1"
|
|
263
|
+
}
|
|
264
|
+
})
|
|
265
|
+
);
|
|
266
|
+
auditSpy.mockRestore();
|
|
267
|
+
});
|
|
268
|
+
it("omits target_resource when an ID path parameter is missing", async () => {
|
|
269
|
+
const endpoint = makeEndpoint({
|
|
270
|
+
alias: "get-drive-item",
|
|
271
|
+
path: "/drives/:driveId/items/:driveItemId",
|
|
272
|
+
parameters: [
|
|
273
|
+
{ name: "driveId", type: "Path", schema: z.string() },
|
|
274
|
+
{ name: "driveItemId", type: "Path", schema: z.string() }
|
|
275
|
+
]
|
|
276
|
+
});
|
|
277
|
+
const config = makeConfig({
|
|
278
|
+
toolName: "get-drive-item",
|
|
279
|
+
pathPattern: "/drives/{drive-id}/items/{driveItem-id}",
|
|
280
|
+
scopes: ["Files.Read"]
|
|
281
|
+
});
|
|
282
|
+
mockEndpoints.push(endpoint);
|
|
283
|
+
mockEndpointsJson = [config];
|
|
284
|
+
const graphClient = createMockGraphClient([
|
|
285
|
+
{ content: [{ type: "text", text: JSON.stringify({ id: "item-2" }) }] }
|
|
286
|
+
]);
|
|
287
|
+
const server = createMockServer();
|
|
288
|
+
const { registerGraphTools } = await loadModule();
|
|
289
|
+
registerGraphTools(server, graphClient);
|
|
290
|
+
const auditSpy = await spyOnAuditLogger();
|
|
291
|
+
await server.tools.get("get-drive-item").handler({
|
|
292
|
+
driveId: "drive-1"
|
|
293
|
+
});
|
|
294
|
+
const [payload] = auditSpy.mock.calls[0];
|
|
295
|
+
expect(payload).toMatchObject({
|
|
296
|
+
event: "tool.call",
|
|
297
|
+
tool: "get-drive-item",
|
|
298
|
+
status: "success"
|
|
299
|
+
});
|
|
300
|
+
expect(payload).not.toHaveProperty("target_resource");
|
|
301
|
+
auditSpy.mockRestore();
|
|
302
|
+
});
|
|
303
|
+
it("omits SharePoint path parameters from target_resource", async () => {
|
|
304
|
+
const endpoint = makeEndpoint({
|
|
305
|
+
alias: "get-sharepoint-site-by-path",
|
|
306
|
+
path: "/sites/:siteId/getByPath(path=':path')",
|
|
307
|
+
parameters: [
|
|
308
|
+
{ name: "siteId", type: "Path", schema: z.string() },
|
|
309
|
+
{ name: "path", type: "Path", schema: z.string() }
|
|
310
|
+
]
|
|
311
|
+
});
|
|
312
|
+
const config = makeConfig({
|
|
313
|
+
toolName: "get-sharepoint-site-by-path",
|
|
314
|
+
pathPattern: "/sites/{site-id}:/{path}",
|
|
315
|
+
scopes: [["Sites.Read.All"], ["Sites.Selected"]]
|
|
316
|
+
});
|
|
317
|
+
mockEndpoints.push(endpoint);
|
|
318
|
+
mockEndpointsJson = [config];
|
|
319
|
+
const graphClient = createMockGraphClient([
|
|
320
|
+
{ content: [{ type: "text", text: JSON.stringify({ id: "site-1" }) }] }
|
|
321
|
+
]);
|
|
322
|
+
const server = createMockServer();
|
|
323
|
+
const { registerGraphTools } = await loadModule();
|
|
324
|
+
registerGraphTools(server, graphClient);
|
|
325
|
+
const auditSpy = await spyOnAuditLogger();
|
|
326
|
+
await server.tools.get("get-sharepoint-site-by-path").handler({
|
|
327
|
+
siteId: "contoso.sharepoint.com",
|
|
328
|
+
path: "/sites/Finance"
|
|
329
|
+
});
|
|
330
|
+
expect(auditSpy).toHaveBeenCalledWith(
|
|
331
|
+
expect.objectContaining({
|
|
332
|
+
event: "tool.call",
|
|
333
|
+
tool: "get-sharepoint-site-by-path",
|
|
334
|
+
status: "success",
|
|
335
|
+
target_resource: {
|
|
336
|
+
type: "site",
|
|
337
|
+
id: "/sites/contoso.sharepoint.com"
|
|
338
|
+
}
|
|
339
|
+
})
|
|
340
|
+
);
|
|
341
|
+
const [payload] = auditSpy.mock.calls[0];
|
|
342
|
+
expect(JSON.stringify(payload)).not.toContain("Finance");
|
|
343
|
+
auditSpy.mockRestore();
|
|
344
|
+
});
|
|
345
|
+
it("omits target_resource for generated broad list/search audit events", async () => {
|
|
346
|
+
const endpoint = makeEndpoint({
|
|
347
|
+
alias: "list-mail-messages",
|
|
348
|
+
path: "/me/messages"
|
|
349
|
+
});
|
|
350
|
+
const config = makeConfig({
|
|
351
|
+
toolName: "list-mail-messages",
|
|
352
|
+
pathPattern: "/me/messages"
|
|
353
|
+
});
|
|
354
|
+
mockEndpoints.push(endpoint);
|
|
355
|
+
mockEndpointsJson = [config];
|
|
356
|
+
const graphClient = createMockGraphClient([
|
|
357
|
+
{ content: [{ type: "text", text: JSON.stringify({ value: [] }) }] }
|
|
358
|
+
]);
|
|
359
|
+
const server = createMockServer();
|
|
360
|
+
const { registerGraphTools } = await loadModule();
|
|
361
|
+
registerGraphTools(server, graphClient);
|
|
362
|
+
const auditSpy = await spyOnAuditLogger();
|
|
363
|
+
await server.tools.get("list-mail-messages").handler({ search: "budget" });
|
|
364
|
+
const [payload] = auditSpy.mock.calls[0];
|
|
365
|
+
expect(payload).toMatchObject({
|
|
366
|
+
event: "tool.call",
|
|
367
|
+
tool: "list-mail-messages",
|
|
368
|
+
status: "success"
|
|
369
|
+
});
|
|
370
|
+
expect(payload).not.toHaveProperty("target_resource");
|
|
371
|
+
auditSpy.mockRestore();
|
|
372
|
+
});
|
|
373
|
+
});
|
|
142
374
|
describe("fetchAllPages pagination", () => {
|
|
143
375
|
it("should follow @odata.nextLink and combine results", async () => {
|
|
144
376
|
const endpoint = makeEndpoint();
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
const CONTROL_PARAM_NAMES = /* @__PURE__ */ new Set([
|
|
2
|
+
"account",
|
|
3
|
+
"confirm",
|
|
4
|
+
"fetchAllPages",
|
|
5
|
+
"includeHeaders",
|
|
6
|
+
"excludeResponse",
|
|
7
|
+
"timezone",
|
|
8
|
+
"expandExtendedProperties"
|
|
9
|
+
]);
|
|
10
|
+
function toCamelCase(name) {
|
|
11
|
+
return name.replace(/-([a-zA-Z])/g, (_, c) => c.toUpperCase());
|
|
12
|
+
}
|
|
13
|
+
function toKebabCase(name) {
|
|
14
|
+
return name.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`);
|
|
15
|
+
}
|
|
16
|
+
function toSnakeCase(name) {
|
|
17
|
+
return name.replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/[-\s]+/g, "_").replace(/_+/g, "_").replace(/^_|_$/g, "").toLowerCase();
|
|
18
|
+
}
|
|
19
|
+
function valueForPlaceholder(placeholderName, params) {
|
|
20
|
+
const candidates = [placeholderName, toCamelCase(placeholderName), toKebabCase(placeholderName)];
|
|
21
|
+
for (const candidate of candidates) {
|
|
22
|
+
if (CONTROL_PARAM_NAMES.has(candidate)) continue;
|
|
23
|
+
const value = params[candidate];
|
|
24
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
25
|
+
return encodeURIComponent(String(value)).replace(/%3D/g, "=");
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return void 0;
|
|
29
|
+
}
|
|
30
|
+
function resolveGraphPathForAudit(pathPattern, params = {}) {
|
|
31
|
+
if (!pathPattern) return void 0;
|
|
32
|
+
let resolvedPath = pathPattern;
|
|
33
|
+
resolvedPath = resolvedPath.replace(/:([A-Za-z][A-Za-z0-9-]*)/g, (match, name) => {
|
|
34
|
+
return valueForPlaceholder(name, params) ?? match;
|
|
35
|
+
});
|
|
36
|
+
resolvedPath = resolvedPath.replace(/\{([^}]+)\}/g, (match, name) => {
|
|
37
|
+
return valueForPlaceholder(name, params) ?? match;
|
|
38
|
+
});
|
|
39
|
+
if (/:([A-Za-z][A-Za-z0-9-]*)|\{[^}]+\}/.test(resolvedPath)) {
|
|
40
|
+
return void 0;
|
|
41
|
+
}
|
|
42
|
+
return resolvedPath.startsWith("/") ? resolvedPath : `/${resolvedPath}`;
|
|
43
|
+
}
|
|
44
|
+
function idPlaceholderBase(name) {
|
|
45
|
+
const base = name.replace(/[-_]?id\d*$/i, "");
|
|
46
|
+
if (base === name || base.length === 0) return void 0;
|
|
47
|
+
return base;
|
|
48
|
+
}
|
|
49
|
+
function deriveTargetResource(input) {
|
|
50
|
+
const pathPattern = input.pathPattern;
|
|
51
|
+
if (!pathPattern) return void 0;
|
|
52
|
+
const placeholderPattern = /\{([^}]+)\}|:([A-Za-z][A-Za-z0-9-]*)/g;
|
|
53
|
+
let lastTarget;
|
|
54
|
+
for (const match of pathPattern.matchAll(placeholderPattern)) {
|
|
55
|
+
const name = match[1] ?? match[2];
|
|
56
|
+
const base = idPlaceholderBase(name);
|
|
57
|
+
if (!base) continue;
|
|
58
|
+
lastTarget = {
|
|
59
|
+
base,
|
|
60
|
+
end: match.index + match[0].length
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
if (!lastTarget) return void 0;
|
|
64
|
+
const targetPattern = pathPattern.slice(0, lastTarget.end);
|
|
65
|
+
const id = resolveGraphPathForAudit(targetPattern, input.params ?? {});
|
|
66
|
+
if (!id) return void 0;
|
|
67
|
+
return {
|
|
68
|
+
type: toSnakeCase(lastTarget.base),
|
|
69
|
+
id
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
export {
|
|
73
|
+
deriveTargetResource,
|
|
74
|
+
resolveGraphPathForAudit
|
|
75
|
+
};
|
package/dist/graph-tools.js
CHANGED
|
@@ -20,6 +20,7 @@ import { TOOL_CATEGORIES } from "./tool-categories.js";
|
|
|
20
20
|
import { getRequestTokens } from "./request-context.js";
|
|
21
21
|
import { parseTeamsUrl } from "./lib/teams-url-parser.js";
|
|
22
22
|
import { buildBM25Index, scoreQuery, tokenize } from "./lib/bm25.js";
|
|
23
|
+
import { deriveTargetResource } from "./audit-target-resource.js";
|
|
23
24
|
import { describeToolSchema, describeUtilityToolSchema } from "./lib/tool-schema.js";
|
|
24
25
|
import {
|
|
25
26
|
TOP_UNSUPPORTED_DELTA_TOOLS,
|
|
@@ -598,6 +599,7 @@ async function executeGraphTool(tool, config, graphClient, params, authManager)
|
|
|
598
599
|
const startTime = Date.now();
|
|
599
600
|
const upn = getUserIdentityForAudit(getRequestTokens()?.accessToken);
|
|
600
601
|
const httpMethod = tool.method.toUpperCase();
|
|
602
|
+
let targetResource;
|
|
601
603
|
try {
|
|
602
604
|
const accountParam = params.account;
|
|
603
605
|
const accountModeError = await checkAccountParamInBearerMode(accountParam, authManager);
|
|
@@ -815,6 +817,10 @@ async function executeGraphTool(tool, config, graphClient, params, authManager)
|
|
|
815
817
|
if (accountAccessToken) {
|
|
816
818
|
options.accessToken = accountAccessToken;
|
|
817
819
|
}
|
|
820
|
+
targetResource = deriveTargetResource({
|
|
821
|
+
pathPattern: config?.pathPattern ?? tool.path,
|
|
822
|
+
params
|
|
823
|
+
});
|
|
818
824
|
const { accessToken: _redacted, ...safeOptions } = options;
|
|
819
825
|
logger.info(
|
|
820
826
|
`Making graph request to ${path2} with options: ${JSON.stringify(safeOptions)}${_redacted ? " [accessToken=REDACTED]" : ""}`
|
|
@@ -915,7 +921,8 @@ async function executeGraphTool(tool, config, graphClient, params, authManager)
|
|
|
915
921
|
tool: tool.alias,
|
|
916
922
|
http_method: httpMethod,
|
|
917
923
|
status: response.isError ? "error" : "success",
|
|
918
|
-
duration_ms: Date.now() - startTime
|
|
924
|
+
duration_ms: Date.now() - startTime,
|
|
925
|
+
...targetResource ? { target_resource: targetResource } : {}
|
|
919
926
|
});
|
|
920
927
|
return {
|
|
921
928
|
content,
|
|
@@ -933,6 +940,7 @@ async function executeGraphTool(tool, config, graphClient, params, authManager)
|
|
|
933
940
|
http_method: httpMethod,
|
|
934
941
|
status: "error",
|
|
935
942
|
duration_ms: Date.now() - startTime,
|
|
943
|
+
...targetResource ? { target_resource: targetResource } : {},
|
|
936
944
|
error_type: err?.name || "Error",
|
|
937
945
|
error_code: err?.status ?? err?.code
|
|
938
946
|
});
|
package/docs/deployment.md
CHANGED
|
@@ -213,7 +213,7 @@ The client automatically discovers OAuth endpoints and opens a browser for authe
|
|
|
213
213
|
- **Tool filtering**: use `--enabled-tools <regex>` or `--preset <names>` to restrict available tools
|
|
214
214
|
- **CORS**: configure `MS365_MCP_CORS_ORIGIN` to restrict allowed origins (defaults to `http://localhost:3000`); set explicitly when clients run on a different origin
|
|
215
215
|
- **Disable Dynamic Client Registration**: when only a known client talks to the server, set `MS365_MCP_DISABLE_DCR=true` (or pass `--no-dynamic-registration`) to close the anonymous `/register` endpoint
|
|
216
|
-
- **Structured audit log**: enabled by default. Every tool invocation emits one JSON line on
|
|
216
|
+
- **Structured audit log**: enabled by default. Every tool invocation emits one JSON line on stderr (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, target_resource?, error_type?, error_code? }`. When an audited generated Microsoft Graph tool targets a derivable resource through an ID-like path parameter such as `{message-id}` or `{driveItem-id}`, `target_resource` is `{ type, id }`, where `id` is the Graph path up to that resource ID. Later path parameters such as `{path}`, query values, tool parameters, returned content, 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`
|
|
217
217
|
- **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`
|
|
218
218
|
- **Confirm gate on destructive tools**: opt-in, **off by default**. Enable with `MS365_MCP_REQUIRE_CONFIRM=true`. When on, destructive tools (POST except `readOnly`, PATCH, PUT, DELETE — `delete-mail-message`, `send-mail`, `update-event`, etc.) return `{ "error": "confirmation_required" }` until the caller re-invokes them with `"confirm": true`. Mitigates accidental writes when an LLM misroutes a request or follows an injected instruction. Shipped opt-in so it is a non-breaking, additive layer that can coexist with client-side elicitation prompts (MCP Elicitation API) where the client supports them.
|
|
219
219
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@softeria/ms-365-mcp-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.136.0",
|
|
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",
|