@softeria/ms-365-mcp-server 0.145.2 → 0.146.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.
|
@@ -333,6 +333,99 @@ describe("graph-tools", () => {
|
|
|
333
333
|
);
|
|
334
334
|
});
|
|
335
335
|
});
|
|
336
|
+
describe("audit recipient metadata", () => {
|
|
337
|
+
const draftEndpoint = () => {
|
|
338
|
+
const endpoint = makeEndpoint({
|
|
339
|
+
method: "post",
|
|
340
|
+
path: "/me/messages",
|
|
341
|
+
alias: "create-draft-email",
|
|
342
|
+
parameters: [{ name: "body", type: "Body", schema: z.object({}).passthrough() }]
|
|
343
|
+
});
|
|
344
|
+
const config = makeConfig({
|
|
345
|
+
pathPattern: "/me/messages",
|
|
346
|
+
method: "post",
|
|
347
|
+
toolName: "create-draft-email"
|
|
348
|
+
});
|
|
349
|
+
mockEndpoints.push(endpoint);
|
|
350
|
+
mockEndpointsJson = [config];
|
|
351
|
+
};
|
|
352
|
+
const runDraft = async (body) => {
|
|
353
|
+
const graphClient = createMockGraphClient([
|
|
354
|
+
{
|
|
355
|
+
content: [{ type: "text", text: JSON.stringify({ id: "draft-1" }) }],
|
|
356
|
+
_meta: { http_status: 201 }
|
|
357
|
+
}
|
|
358
|
+
]);
|
|
359
|
+
const server = createMockServer();
|
|
360
|
+
const { registerGraphTools } = await loadModule();
|
|
361
|
+
registerGraphTools(
|
|
362
|
+
server,
|
|
363
|
+
graphClient
|
|
364
|
+
);
|
|
365
|
+
await server.tools.get("create-draft-email").handler({ body });
|
|
366
|
+
return auditLogMock.mock.calls[0][0];
|
|
367
|
+
};
|
|
368
|
+
it("records recipient count and domains, deduplicated and sorted", async () => {
|
|
369
|
+
draftEndpoint();
|
|
370
|
+
const payload = await runDraft({
|
|
371
|
+
toRecipients: [
|
|
372
|
+
{ emailAddress: { address: "someone@example.com" } },
|
|
373
|
+
{ emailAddress: { address: "Another@Example.com" } }
|
|
374
|
+
],
|
|
375
|
+
ccRecipients: [{ emailAddress: { address: "auditor@partner.co.uk" } }]
|
|
376
|
+
});
|
|
377
|
+
expect(payload).toMatchObject({
|
|
378
|
+
tool: "create-draft-email",
|
|
379
|
+
recipient_count: 3,
|
|
380
|
+
recipient_domains: ["example.com", "partner.co.uk"]
|
|
381
|
+
});
|
|
382
|
+
});
|
|
383
|
+
it("reads recipients nested under a camelCase message, as createReply sends them", async () => {
|
|
384
|
+
draftEndpoint();
|
|
385
|
+
const payload = await runDraft({
|
|
386
|
+
comment: "forwarding this on",
|
|
387
|
+
message: { toRecipients: [{ emailAddress: { address: "outside@gmail.com" } }] }
|
|
388
|
+
});
|
|
389
|
+
expect(payload).toMatchObject({ recipient_count: 1, recipient_domains: ["gmail.com"] });
|
|
390
|
+
});
|
|
391
|
+
it("reads PascalCase fields, as the Graph action endpoints send them", async () => {
|
|
392
|
+
draftEndpoint();
|
|
393
|
+
const payload = await runDraft({
|
|
394
|
+
Comment: "fyi",
|
|
395
|
+
ToRecipients: [{ emailAddress: { address: "partner@vendor.com" } }],
|
|
396
|
+
Message: { CcRecipients: [{ emailAddress: { address: "watcher@vendor.com" } }] }
|
|
397
|
+
});
|
|
398
|
+
expect(payload).toMatchObject({ recipient_count: 2, recipient_domains: ["vendor.com"] });
|
|
399
|
+
});
|
|
400
|
+
it("records calendar attendees, not just mail recipients", async () => {
|
|
401
|
+
draftEndpoint();
|
|
402
|
+
const payload = await runDraft({
|
|
403
|
+
subject: "sync",
|
|
404
|
+
attendees: [
|
|
405
|
+
{ emailAddress: { address: "colleague@example.com" }, type: "required" },
|
|
406
|
+
{ emailAddress: { address: "guest@external.org" }, type: "optional" }
|
|
407
|
+
]
|
|
408
|
+
});
|
|
409
|
+
expect(payload).toMatchObject({
|
|
410
|
+
recipient_count: 2,
|
|
411
|
+
recipient_domains: ["example.com", "external.org"]
|
|
412
|
+
});
|
|
413
|
+
});
|
|
414
|
+
it("logs domains only, never the local part of an address", async () => {
|
|
415
|
+
draftEndpoint();
|
|
416
|
+
const payload = await runDraft({
|
|
417
|
+
toRecipients: [{ emailAddress: { address: "confidential.name@example.com" } }]
|
|
418
|
+
});
|
|
419
|
+
expect(payload.recipient_domains).toEqual(["example.com"]);
|
|
420
|
+
expect(JSON.stringify(payload)).not.toContain("confidential.name");
|
|
421
|
+
});
|
|
422
|
+
it("omits both fields when a request has no recipients", async () => {
|
|
423
|
+
draftEndpoint();
|
|
424
|
+
const payload = await runDraft({ subject: "a draft with no recipients yet" });
|
|
425
|
+
expect(payload).not.toHaveProperty("recipient_count");
|
|
426
|
+
expect(payload).not.toHaveProperty("recipient_domains");
|
|
427
|
+
});
|
|
428
|
+
});
|
|
336
429
|
describe("$count advanced query mode", () => {
|
|
337
430
|
it("should set ConsistencyLevel: eventual header when $count=true", async () => {
|
|
338
431
|
const endpoint = makeEndpoint();
|
package/dist/graph-tools.js
CHANGED
|
@@ -115,6 +115,39 @@ function graphResponseAuditFields(response) {
|
|
|
115
115
|
...graphBatchErrorCodeCounts !== void 0 ? { graph_batch_error_code_counts: graphBatchErrorCodeCounts } : {}
|
|
116
116
|
};
|
|
117
117
|
}
|
|
118
|
+
const RECIPIENT_FIELDS = /* @__PURE__ */ new Set(["torecipients", "ccrecipients", "bccrecipients", "attendees"]);
|
|
119
|
+
const NESTED_BODY_FIELDS = /* @__PURE__ */ new Set(["message"]);
|
|
120
|
+
const MAX_BODY_DEPTH = 3;
|
|
121
|
+
function collectRecipients(node, domains, counter, depth = 0) {
|
|
122
|
+
if (!node || typeof node !== "object" || depth > MAX_BODY_DEPTH) return;
|
|
123
|
+
for (const [key, value] of Object.entries(node)) {
|
|
124
|
+
const name = key.toLowerCase();
|
|
125
|
+
if (NESTED_BODY_FIELDS.has(name)) {
|
|
126
|
+
collectRecipients(value, domains, counter, depth + 1);
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
if (!RECIPIENT_FIELDS.has(name) || !Array.isArray(value)) continue;
|
|
130
|
+
for (const entry of value) {
|
|
131
|
+
const address = entry?.emailAddress?.address;
|
|
132
|
+
if (typeof address !== "string") continue;
|
|
133
|
+
counter.count += 1;
|
|
134
|
+
const at = address.lastIndexOf("@");
|
|
135
|
+
if (at > 0 && at < address.length - 1) {
|
|
136
|
+
domains.add(address.slice(at + 1).toLowerCase());
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
function recipientAuditFields(body) {
|
|
142
|
+
const domains = /* @__PURE__ */ new Set();
|
|
143
|
+
const counter = { count: 0 };
|
|
144
|
+
collectRecipients(body, domains, counter);
|
|
145
|
+
if (counter.count === 0) return {};
|
|
146
|
+
return {
|
|
147
|
+
recipient_count: counter.count,
|
|
148
|
+
...domains.size > 0 ? { recipient_domains: [...domains].sort() } : {}
|
|
149
|
+
};
|
|
150
|
+
}
|
|
118
151
|
function thrownErrorAuditFields(error) {
|
|
119
152
|
const err = error;
|
|
120
153
|
const httpStatus = auditHttpStatus(err?.httpStatus ?? err?.status);
|
|
@@ -1130,7 +1163,8 @@ async function executeGraphTool(tool, config, graphClient, params, authManager)
|
|
|
1130
1163
|
status: response.isError ? "error" : "success",
|
|
1131
1164
|
duration_ms: Date.now() - startTime,
|
|
1132
1165
|
...targetResource ? { target_resource: targetResource } : {},
|
|
1133
|
-
...graphResponseAuditFields(response)
|
|
1166
|
+
...graphResponseAuditFields(response),
|
|
1167
|
+
...recipientAuditFields(body)
|
|
1134
1168
|
});
|
|
1135
1169
|
return {
|
|
1136
1170
|
content,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@softeria/ms-365-mcp-server",
|
|
3
3
|
"mcpName": "io.github.Softeria/ms-365-mcp-server",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.146.0",
|
|
5
5
|
"description": " A Model Context Protocol (MCP) server for interacting with Microsoft 365 and Office services through the Graph API",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "dist/index.js",
|