@softeria/ms-365-mcp-server 0.46.1 → 0.46.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.
@@ -0,0 +1,355 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ import { z } from "zod";
3
+ vi.mock("../logger.js", () => ({
4
+ default: {
5
+ info: vi.fn(),
6
+ warn: vi.fn(),
7
+ error: vi.fn(),
8
+ debug: vi.fn()
9
+ }
10
+ }));
11
+ const mockEndpoints = [];
12
+ vi.mock("../generated/client.js", () => ({
13
+ api: {
14
+ get endpoints() {
15
+ return mockEndpoints;
16
+ }
17
+ }
18
+ }));
19
+ let mockEndpointsJson = [];
20
+ vi.mock("fs", async (importOriginal) => {
21
+ const actual = await importOriginal();
22
+ return {
23
+ ...actual,
24
+ readFileSync: (filePath, encoding) => {
25
+ if (typeof filePath === "string" && filePath.includes("endpoints.json")) {
26
+ return JSON.stringify(mockEndpointsJson);
27
+ }
28
+ return actual.readFileSync(filePath, encoding);
29
+ }
30
+ };
31
+ });
32
+ vi.mock("../tool-categories.js", () => ({
33
+ TOOL_CATEGORIES: {}
34
+ }));
35
+ function makeEndpoint(overrides = {}) {
36
+ return {
37
+ method: "get",
38
+ path: "/me/messages",
39
+ alias: "test-tool",
40
+ description: "Test tool",
41
+ requestFormat: "json",
42
+ parameters: [
43
+ { name: "filter", type: "Query", schema: z.string().optional() },
44
+ { name: "search", type: "Query", schema: z.string().optional() },
45
+ { name: "select", type: "Query", schema: z.string().optional() },
46
+ { name: "orderby", type: "Query", schema: z.string().optional() },
47
+ { name: "count", type: "Query", schema: z.boolean().optional() },
48
+ { name: "top", type: "Query", schema: z.number().optional() },
49
+ { name: "skip", type: "Query", schema: z.number().optional() }
50
+ ],
51
+ response: z.any(),
52
+ ...overrides
53
+ };
54
+ }
55
+ function makeConfig(overrides = {}) {
56
+ return {
57
+ pathPattern: "/me/messages",
58
+ method: "get",
59
+ toolName: "test-tool",
60
+ scopes: ["Mail.Read"],
61
+ ...overrides
62
+ };
63
+ }
64
+ function createMockGraphClient(responses) {
65
+ const responseQueue = [...responses || []];
66
+ return {
67
+ graphRequest: vi.fn().mockImplementation(async () => {
68
+ if (responseQueue.length > 0) {
69
+ return responseQueue.shift();
70
+ }
71
+ return {
72
+ content: [{ type: "text", text: JSON.stringify({ value: [] }) }]
73
+ };
74
+ })
75
+ };
76
+ }
77
+ async function loadModule() {
78
+ vi.resetModules();
79
+ const mod = await import("../graph-tools.js");
80
+ return mod;
81
+ }
82
+ function createMockServer() {
83
+ const tools = /* @__PURE__ */ new Map();
84
+ return {
85
+ tool: vi.fn(
86
+ (name, description, schema, annotations, handler) => {
87
+ tools.set(name, { description, schema, handler });
88
+ }
89
+ ),
90
+ tools
91
+ };
92
+ }
93
+ describe("graph-tools", () => {
94
+ beforeEach(() => {
95
+ mockEndpoints.length = 0;
96
+ mockEndpointsJson = [];
97
+ vi.clearAllMocks();
98
+ });
99
+ describe("$count advanced query mode", () => {
100
+ it("should set ConsistencyLevel: eventual header when $count=true", async () => {
101
+ const endpoint = makeEndpoint();
102
+ const config = makeConfig();
103
+ mockEndpoints.push(endpoint);
104
+ mockEndpointsJson = [config];
105
+ const graphClient = createMockGraphClient([
106
+ { content: [{ type: "text", text: JSON.stringify({ value: [] }) }] }
107
+ ]);
108
+ const server = createMockServer();
109
+ const { registerGraphTools } = await loadModule();
110
+ registerGraphTools(server, graphClient);
111
+ const tool = server.tools.get("test-tool");
112
+ expect(tool).toBeDefined();
113
+ await tool.handler({ count: true });
114
+ expect(graphClient.graphRequest).toHaveBeenCalledTimes(1);
115
+ const [url] = graphClient.graphRequest.mock.calls[0];
116
+ expect(url).toContain("$count=true");
117
+ });
118
+ });
119
+ describe("fetchAllPages pagination", () => {
120
+ it("should follow @odata.nextLink and combine results", async () => {
121
+ const endpoint = makeEndpoint();
122
+ const config = makeConfig();
123
+ mockEndpoints.push(endpoint);
124
+ mockEndpointsJson = [config];
125
+ const graphClient = createMockGraphClient([
126
+ {
127
+ content: [
128
+ {
129
+ type: "text",
130
+ text: JSON.stringify({
131
+ value: [{ id: "1" }, { id: "2" }],
132
+ "@odata.nextLink": "https://graph.microsoft.com/v1.0/me/messages?$skip=2"
133
+ })
134
+ }
135
+ ]
136
+ },
137
+ {
138
+ content: [
139
+ {
140
+ type: "text",
141
+ text: JSON.stringify({
142
+ value: [{ id: "3" }]
143
+ })
144
+ }
145
+ ]
146
+ }
147
+ ]);
148
+ const server = createMockServer();
149
+ const { registerGraphTools } = await loadModule();
150
+ registerGraphTools(server, graphClient);
151
+ const tool = server.tools.get("test-tool");
152
+ const result = await tool.handler({ fetchAllPages: true });
153
+ expect(graphClient.graphRequest).toHaveBeenCalledTimes(2);
154
+ const parsed = JSON.parse(result.content[0].text);
155
+ expect(parsed.value).toHaveLength(3);
156
+ expect(parsed.value.map((v) => v.id)).toEqual(["1", "2", "3"]);
157
+ expect(parsed["@odata.nextLink"]).toBeUndefined();
158
+ });
159
+ it("should stop at 100 page limit", async () => {
160
+ const endpoint = makeEndpoint();
161
+ const config = makeConfig();
162
+ mockEndpoints.push(endpoint);
163
+ mockEndpointsJson = [config];
164
+ const responses = [];
165
+ for (let i = 0; i < 101; i++) {
166
+ responses.push({
167
+ content: [
168
+ {
169
+ type: "text",
170
+ text: JSON.stringify({
171
+ value: [{ id: `item-${i}` }],
172
+ "@odata.nextLink": "https://graph.microsoft.com/v1.0/me/messages?$skip=" + (i + 1)
173
+ })
174
+ }
175
+ ]
176
+ });
177
+ }
178
+ const graphClient = createMockGraphClient(responses);
179
+ const server = createMockServer();
180
+ const { registerGraphTools } = await loadModule();
181
+ registerGraphTools(server, graphClient);
182
+ const tool = server.tools.get("test-tool");
183
+ await tool.handler({ fetchAllPages: true });
184
+ expect(graphClient.graphRequest).toHaveBeenCalledTimes(100);
185
+ });
186
+ });
187
+ describe("parameter describe() overrides", () => {
188
+ it("should apply custom descriptions to OData parameters", async () => {
189
+ const endpoint = makeEndpoint();
190
+ const config = makeConfig();
191
+ mockEndpoints.push(endpoint);
192
+ mockEndpointsJson = [config];
193
+ const server = createMockServer();
194
+ const { registerGraphTools } = await loadModule();
195
+ registerGraphTools(server, createMockGraphClient());
196
+ const tool = server.tools.get("test-tool");
197
+ expect(tool).toBeDefined();
198
+ const schema = tool.schema;
199
+ expect(schema["filter"]).toBeDefined();
200
+ expect(schema["filter"].description).toContain("OData filter expression");
201
+ expect(schema["filter"].description).toContain("$count=true");
202
+ expect(schema["search"]).toBeDefined();
203
+ expect(schema["search"].description).toContain("KQL search query");
204
+ expect(schema["select"]).toBeDefined();
205
+ expect(schema["select"].description).toContain("Comma-separated fields");
206
+ expect(schema["orderby"]).toBeDefined();
207
+ expect(schema["orderby"].description).toContain("Sort expression");
208
+ expect(schema["count"]).toBeDefined();
209
+ expect(schema["count"].description).toContain("advanced query mode");
210
+ });
211
+ });
212
+ describe("returnDownloadUrl", () => {
213
+ it("should strip /content from path and return downloadUrl when returnDownloadUrl=true", async () => {
214
+ const endpoint = makeEndpoint({
215
+ alias: "download-file",
216
+ path: "/me/drive/items/:driveItem-id/content",
217
+ parameters: [{ name: "driveItem-id", type: "Path", schema: z.string() }]
218
+ });
219
+ const config = makeConfig({
220
+ toolName: "download-file",
221
+ pathPattern: "/me/drive/items/{driveItem-id}/content",
222
+ returnDownloadUrl: true
223
+ });
224
+ mockEndpoints.push(endpoint);
225
+ mockEndpointsJson = [config];
226
+ const downloadUrl = "https://download.example.com/file.pdf";
227
+ const graphClient = createMockGraphClient([
228
+ {
229
+ content: [
230
+ {
231
+ type: "text",
232
+ text: JSON.stringify({
233
+ "@microsoft.graph.downloadUrl": downloadUrl,
234
+ name: "file.pdf"
235
+ })
236
+ }
237
+ ]
238
+ }
239
+ ]);
240
+ const server = createMockServer();
241
+ const { registerGraphTools } = await loadModule();
242
+ registerGraphTools(server, graphClient);
243
+ const tool = server.tools.get("download-file");
244
+ expect(tool).toBeDefined();
245
+ await tool.handler({ "driveItem-id": "abc123" });
246
+ const [requestedPath] = graphClient.graphRequest.mock.calls[0];
247
+ expect(requestedPath).not.toContain("/content");
248
+ expect(requestedPath).toContain("/me/drive/items/abc123");
249
+ });
250
+ });
251
+ describe("kebab-case path param normalization", () => {
252
+ it("should substitute path when LLM passes message-id (kebab) but schema has messageId (camelCase)", async () => {
253
+ const endpoint = makeEndpoint({
254
+ alias: "get-mail-message",
255
+ method: "get",
256
+ path: "/me/messages/:messageId",
257
+ parameters: [
258
+ { name: "messageId", type: "Path", schema: z.string() },
259
+ { name: "select", type: "Query", schema: z.string().optional() }
260
+ ]
261
+ });
262
+ const config = makeConfig({
263
+ toolName: "get-mail-message",
264
+ pathPattern: "/me/messages/{message-id}"
265
+ });
266
+ mockEndpoints.push(endpoint);
267
+ mockEndpointsJson = [config];
268
+ const graphClient = createMockGraphClient([
269
+ { content: [{ type: "text", text: JSON.stringify({ id: "AAMk123", subject: "Test" }) }] }
270
+ ]);
271
+ const server = createMockServer();
272
+ const { registerGraphTools } = await loadModule();
273
+ registerGraphTools(server, graphClient);
274
+ const tool = server.tools.get("get-mail-message");
275
+ expect(tool).toBeDefined();
276
+ await tool.handler({ "message-id": "AAMk123abc=" });
277
+ const [requestedPath] = graphClient.graphRequest.mock.calls[0];
278
+ expect(requestedPath).toContain("AAMk123abc=");
279
+ expect(requestedPath).not.toContain(":messageId");
280
+ });
281
+ it("should also work when LLM passes messageId (camelCase) directly", async () => {
282
+ const endpoint = makeEndpoint({
283
+ alias: "get-mail-message2",
284
+ method: "get",
285
+ path: "/me/messages/:messageId",
286
+ parameters: [{ name: "messageId", type: "Path", schema: z.string() }]
287
+ });
288
+ const config = makeConfig({
289
+ toolName: "get-mail-message2",
290
+ pathPattern: "/me/messages/{message-id}"
291
+ });
292
+ mockEndpoints.push(endpoint);
293
+ mockEndpointsJson = [config];
294
+ const graphClient = createMockGraphClient([
295
+ { content: [{ type: "text", text: JSON.stringify({ id: "AAMk456" }) }] }
296
+ ]);
297
+ const server = createMockServer();
298
+ const { registerGraphTools } = await loadModule();
299
+ registerGraphTools(server, graphClient);
300
+ const tool = server.tools.get("get-mail-message2");
301
+ await tool.handler({ messageId: "AAMk456xyz=" });
302
+ const [requestedPath] = graphClient.graphRequest.mock.calls[0];
303
+ expect(requestedPath).toContain("AAMk456xyz=");
304
+ expect(requestedPath).not.toContain(":messageId");
305
+ });
306
+ });
307
+ describe("supportsTimezone", () => {
308
+ it("should set Prefer: outlook.timezone header when timezone param provided", async () => {
309
+ const endpoint = makeEndpoint({
310
+ alias: "list-calendar-events",
311
+ path: "/me/events",
312
+ parameters: []
313
+ });
314
+ const config = makeConfig({
315
+ toolName: "list-calendar-events",
316
+ pathPattern: "/me/events",
317
+ supportsTimezone: true
318
+ });
319
+ mockEndpoints.push(endpoint);
320
+ mockEndpointsJson = [config];
321
+ const graphClient = createMockGraphClient([
322
+ { content: [{ type: "text", text: JSON.stringify({ value: [] }) }] }
323
+ ]);
324
+ const server = createMockServer();
325
+ const { registerGraphTools } = await loadModule();
326
+ registerGraphTools(server, graphClient);
327
+ const tool = server.tools.get("list-calendar-events");
328
+ expect(tool).toBeDefined();
329
+ expect(tool.schema["timezone"]).toBeDefined();
330
+ expect(tool.schema["timezone"].description).toContain("IANA timezone");
331
+ await tool.handler({ timezone: "Europe/Brussels" });
332
+ const [, options] = graphClient.graphRequest.mock.calls[0];
333
+ expect(options.headers["Prefer"]).toContain('outlook.timezone="Europe/Brussels"');
334
+ });
335
+ it("should NOT add timezone parameter when supportsTimezone is false/absent", async () => {
336
+ const endpoint = makeEndpoint({
337
+ alias: "list-mail",
338
+ path: "/me/messages",
339
+ parameters: []
340
+ });
341
+ const config = makeConfig({
342
+ toolName: "list-mail",
343
+ pathPattern: "/me/messages"
344
+ // no supportsTimezone
345
+ });
346
+ mockEndpoints.push(endpoint);
347
+ mockEndpointsJson = [config];
348
+ const server = createMockServer();
349
+ const { registerGraphTools } = await loadModule();
350
+ registerGraphTools(server, createMockGraphClient());
351
+ const tool = server.tools.get("list-mail");
352
+ expect(tool.schema["timezone"]).toBeUndefined();
353
+ });
354
+ });
355
+ });
@@ -110,13 +110,15 @@
110
110
  "pathPattern": "/me/messages/{message-id}",
111
111
  "method": "delete",
112
112
  "toolName": "delete-mail-message",
113
- "scopes": ["Mail.ReadWrite"]
113
+ "scopes": ["Mail.ReadWrite"],
114
+ "llmTip": "Soft delete — moves to Deleted Items. To permanently delete, delete again from Deleted Items."
114
115
  },
115
116
  {
116
117
  "pathPattern": "/me/messages/{message-id}/move",
117
118
  "method": "post",
118
119
  "toolName": "move-mail-message",
119
- "scopes": ["Mail.ReadWrite"]
120
+ "scopes": ["Mail.ReadWrite"],
121
+ "llmTip": "destinationId accepts folder ID or well-known name (inbox, drafts, sentitems, deleteditems, junkemail, archive)."
120
122
  },
121
123
  {
122
124
  "pathPattern": "/me/messages/{message-id}",
@@ -128,7 +130,8 @@
128
130
  "pathPattern": "/me/messages/{message-id}/attachments",
129
131
  "method": "post",
130
132
  "toolName": "add-mail-attachment",
131
- "scopes": ["Mail.ReadWrite"]
133
+ "scopes": ["Mail.ReadWrite"],
134
+ "llmTip": "Max 3MB. Body requires @odata.type: {\"@odata.type\": \"#microsoft.graph.fileAttachment\", \"name\": \"file.pdf\", \"contentBytes\": \"<base64>\"}."
132
135
  },
133
136
  {
134
137
  "pathPattern": "/me/messages/{message-id}/attachments",
@@ -192,7 +195,8 @@
192
195
  "pathPattern": "/me/messages/{message-id}/send",
193
196
  "method": "post",
194
197
  "toolName": "send-draft-message",
195
- "scopes": ["Mail.Send"]
198
+ "scopes": ["Mail.Send"],
199
+ "llmTip": "No request body needed — just call with the message ID. Draft must exist in Drafts folder."
196
200
  },
197
201
  {
198
202
  "pathPattern": "/me/events",
@@ -200,7 +204,8 @@
200
204
  "toolName": "list-calendar-events",
201
205
  "scopes": ["Calendars.Read"],
202
206
  "supportsTimezone": true,
203
- "supportsExpandExtendedProperties": true
207
+ "supportsExpandExtendedProperties": true,
208
+ "llmTip": "WARNING: Does NOT expand recurring events — only returns seriesMaster. Use get-calendar-view instead to see individual occurrences within a date range."
204
209
  },
205
210
  {
206
211
  "pathPattern": "/me/events/{event-id}",
@@ -222,13 +227,14 @@
222
227
  "method": "patch",
223
228
  "toolName": "update-calendar-event",
224
229
  "scopes": ["Calendars.ReadWrite"],
225
- "llmTip": "CRITICAL: Do not try to guess the email address of the recipients. Use the list-users tool to find the email address of the recipients."
230
+ "llmTip": "CRITICAL: Do not try to guess the email address of the recipients. Use the list-users tool to find the email address of the recipients. WARNING: Setting attendees replaces the entire attendee list — include all attendees, not just new ones."
226
231
  },
227
232
  {
228
233
  "pathPattern": "/me/events/{event-id}",
229
234
  "method": "delete",
230
235
  "toolName": "delete-calendar-event",
231
- "scopes": ["Calendars.ReadWrite"]
236
+ "scopes": ["Calendars.ReadWrite"],
237
+ "llmTip": "Deleting a seriesMaster deletes ALL occurrences of the recurring event. To cancel a single occurrence, delete that specific instance ID from list-calendar-event-instances."
232
238
  },
233
239
  {
234
240
  "pathPattern": "/me/calendars/{calendar-id}/events",
@@ -236,7 +242,8 @@
236
242
  "toolName": "list-specific-calendar-events",
237
243
  "scopes": ["Calendars.Read"],
238
244
  "supportsTimezone": true,
239
- "supportsExpandExtendedProperties": true
245
+ "supportsExpandExtendedProperties": true,
246
+ "llmTip": "WARNING: Does NOT expand recurring events — only returns seriesMaster. Use get-specific-calendar-view instead."
240
247
  },
241
248
  {
242
249
  "pathPattern": "/me/calendars/{calendar-id}/events/{event-id}",
@@ -258,13 +265,14 @@
258
265
  "method": "patch",
259
266
  "toolName": "update-specific-calendar-event",
260
267
  "scopes": ["Calendars.ReadWrite"],
261
- "llmTip": "CRITICAL: Do not try to guess the email address of the recipients. Use the list-users tool to find the email address of the recipients."
268
+ "llmTip": "CRITICAL: Do not try to guess the email address of the recipients. Use the list-users tool to find the email address of the recipients. WARNING: Setting attendees replaces the entire attendee list — include all attendees, not just new ones."
262
269
  },
263
270
  {
264
271
  "pathPattern": "/me/calendars/{calendar-id}/events/{event-id}",
265
272
  "method": "delete",
266
273
  "toolName": "delete-specific-calendar-event",
267
- "scopes": ["Calendars.ReadWrite"]
274
+ "scopes": ["Calendars.ReadWrite"],
275
+ "llmTip": "Deleting a seriesMaster deletes ALL occurrences. To cancel a single occurrence, use the specific instance ID."
268
276
  },
269
277
  {
270
278
  "pathPattern": "/me/calendarView",
@@ -334,7 +342,8 @@
334
342
  "method": "get",
335
343
  "toolName": "download-onedrive-file-content",
336
344
  "scopes": ["Files.Read"],
337
- "returnDownloadUrl": true
345
+ "returnDownloadUrl": true,
346
+ "llmTip": "Returns a temporary download URL, NOT the file content directly."
338
347
  },
339
348
  {
340
349
  "pathPattern": "/drives/{drive-id}/items/{driveItem-id}",
@@ -346,7 +355,8 @@
346
355
  "pathPattern": "/drives/{drive-id}/items/{driveItem-id}/content",
347
356
  "method": "put",
348
357
  "toolName": "upload-file-content",
349
- "scopes": ["Files.ReadWrite"]
358
+ "scopes": ["Files.ReadWrite"],
359
+ "llmTip": "Max 4MB. For new files use path format: /items/root:/path/to/file.txt:/content. Overwrites existing files without warning."
350
360
  },
351
361
  {
352
362
  "pathPattern": "/drives/{drive-id}/items/{driveItem-id}/workbook/worksheets/{workbookWorksheet-id}/charts/add",
@@ -413,14 +423,16 @@
413
423
  "method": "post",
414
424
  "toolName": "create-onenote-page",
415
425
  "scopes": ["Notes.Create"],
416
- "contentType": "text/html"
426
+ "contentType": "text/html",
427
+ "llmTip": "Body must be a full HTML document (with <html><head><title>...</title></head><body>...</body></html>). Partial HTML or plain text fails silently or creates malformed pages."
417
428
  },
418
429
  {
419
430
  "pathPattern": "/me/onenote/sections/{onenoteSection-id}/pages",
420
431
  "method": "post",
421
432
  "toolName": "create-onenote-section-page",
422
433
  "scopes": ["Notes.Create"],
423
- "contentType": "text/html"
434
+ "contentType": "text/html",
435
+ "llmTip": "Body must be a full HTML document (with <html><head><title>...</title></head><body>...</body></html>). Partial HTML fails silently."
424
436
  },
425
437
  {
426
438
  "pathPattern": "/me/todo/lists",
@@ -465,7 +477,8 @@
465
477
  "pathPattern": "/me/planner/tasks",
466
478
  "method": "get",
467
479
  "toolName": "list-planner-tasks",
468
- "scopes": ["Tasks.Read"]
480
+ "scopes": ["Tasks.Read"],
481
+ "llmTip": "Priority values: 0=Urgent, 1=Important, 3=Medium, 5=Low, 9=unset."
469
482
  },
470
483
  {
471
484
  "pathPattern": "/planner/plans/{plannerPlan-id}",
@@ -477,13 +490,15 @@
477
490
  "pathPattern": "/planner/plans/{plannerPlan-id}/tasks",
478
491
  "method": "get",
479
492
  "toolName": "list-plan-tasks",
480
- "scopes": ["Tasks.Read"]
493
+ "scopes": ["Tasks.Read"],
494
+ "llmTip": "Priority values: 0=Urgent, 1=Important, 3=Medium, 5=Low, 9=unset."
481
495
  },
482
496
  {
483
497
  "pathPattern": "/planner/tasks/{plannerTask-id}",
484
498
  "method": "get",
485
499
  "toolName": "get-planner-task",
486
- "scopes": ["Tasks.Read"]
500
+ "scopes": ["Tasks.Read"],
501
+ "llmTip": "Response includes @odata.etag — save it, required as If-Match header for update-planner-task. Use includeHeaders=true to capture it."
487
502
  },
488
503
  {
489
504
  "pathPattern": "/planner/tasks",
@@ -495,25 +510,29 @@
495
510
  "pathPattern": "/planner/tasks/{plannerTask-id}",
496
511
  "method": "patch",
497
512
  "toolName": "update-planner-task",
498
- "scopes": ["Tasks.ReadWrite"]
513
+ "scopes": ["Tasks.ReadWrite"],
514
+ "llmTip": "CRITICAL: Requires If-Match header with the task's @odata.etag value, otherwise returns 412 Precondition Failed. Get the ETag from get-planner-task with includeHeaders=true. Priority values: 0=Urgent, 1=Important, 3=Medium, 5=Low, 9=unset."
499
515
  },
500
516
  {
501
517
  "pathPattern": "/planner/tasks/{plannerTask-id}/details",
502
518
  "method": "get",
503
519
  "toolName": "get-planner-task-details",
504
- "scopes": ["Tasks.Read"]
520
+ "scopes": ["Tasks.Read"],
521
+ "llmTip": "Response includes @odata.etag — required for update-planner-task-details. Use includeHeaders=true."
505
522
  },
506
523
  {
507
524
  "pathPattern": "/planner/tasks/{plannerTask-id}/details",
508
525
  "method": "patch",
509
526
  "toolName": "update-planner-task-details",
510
- "scopes": ["Tasks.ReadWrite"]
527
+ "scopes": ["Tasks.ReadWrite"],
528
+ "llmTip": "CRITICAL: Requires If-Match header with ETag from get-planner-task-details (use includeHeaders=true). Checklist items use GUID keys: {\"checklist\": {\"<guid>\": {\"title\": \"...\", \"isChecked\": false}}}."
511
529
  },
512
530
  {
513
531
  "pathPattern": "/me/contacts",
514
532
  "method": "get",
515
533
  "toolName": "list-outlook-contacts",
516
- "scopes": ["Contacts.Read"]
534
+ "scopes": ["Contacts.Read"],
535
+ "llmTip": "$filter only supports startswith() — contains() and eq on emailAddresses do not work. Use $search as alternative for broader matching."
517
536
  },
518
537
  {
519
538
  "pathPattern": "/me/contacts/{contact-id}",
@@ -531,7 +550,8 @@
531
550
  "pathPattern": "/me/contacts/{contact-id}",
532
551
  "method": "patch",
533
552
  "toolName": "update-outlook-contact",
534
- "scopes": ["Contacts.ReadWrite"]
553
+ "scopes": ["Contacts.ReadWrite"],
554
+ "llmTip": "emailAddresses array is replaced entirely — include all addresses, not just new ones."
535
555
  },
536
556
  {
537
557
  "pathPattern": "/me/contacts/{contact-id}",
@@ -573,7 +593,8 @@
573
593
  "pathPattern": "/chats/{chat-id}/messages",
574
594
  "method": "post",
575
595
  "toolName": "send-chat-message",
576
- "workScopes": ["ChatMessage.Send"]
596
+ "workScopes": ["ChatMessage.Send"],
597
+ "llmTip": "Use contentType 'html' in the body — plain text contentType gets mangled by Graph API."
577
598
  },
578
599
  {
579
600
  "pathPattern": "/me/joinedTeams",
@@ -615,13 +636,15 @@
615
636
  "pathPattern": "/teams/{team-id}/channels/{channel-id}/messages",
616
637
  "method": "post",
617
638
  "toolName": "send-channel-message",
618
- "workScopes": ["ChannelMessage.Send"]
639
+ "workScopes": ["ChannelMessage.Send"],
640
+ "llmTip": "Use contentType 'html' in the body — plain text contentType gets mangled by Graph API."
619
641
  },
620
642
  {
621
643
  "pathPattern": "/teams/{team-id}/channels/{channel-id}/messages/{chatMessage-id}/replies",
622
644
  "method": "post",
623
645
  "toolName": "reply-to-channel-message",
624
- "workScopes": ["ChannelMessage.Send"]
646
+ "workScopes": ["ChannelMessage.Send"],
647
+ "llmTip": "Use contentType 'html' in the body — plain text contentType gets mangled by Graph API."
625
648
  },
626
649
  {
627
650
  "pathPattern": "/teams/{team-id}/channels/{channel-id}/messages/{chatMessage-id}/replies",
@@ -645,7 +668,8 @@
645
668
  "pathPattern": "/chats/{chat-id}/messages/{chatMessage-id}/replies",
646
669
  "method": "post",
647
670
  "toolName": "reply-to-chat-message",
648
- "workScopes": ["ChatMessage.Send"]
671
+ "workScopes": ["ChatMessage.Send"],
672
+ "llmTip": "Use contentType 'html' in the body — plain text contentType gets mangled by Graph API."
649
673
  },
650
674
  {
651
675
  "pathPattern": "/sites",
@@ -699,13 +723,15 @@
699
723
  "pathPattern": "/sites/{site-id}/lists/{list-id}/items",
700
724
  "method": "get",
701
725
  "toolName": "list-sharepoint-site-list-items",
702
- "workScopes": ["Sites.Read.All"]
726
+ "workScopes": ["Sites.Read.All"],
727
+ "llmTip": "Add $expand=fields to include actual column values. Without it, only metadata is returned."
703
728
  },
704
729
  {
705
730
  "pathPattern": "/sites/{site-id}/lists/{list-id}/items/{listItem-id}",
706
731
  "method": "get",
707
732
  "toolName": "get-sharepoint-site-list-item",
708
- "workScopes": ["Sites.Read.All"]
733
+ "workScopes": ["Sites.Read.All"],
734
+ "llmTip": "Add $expand=fields to include actual column values. Without it, only metadata is returned."
709
735
  },
710
736
  {
711
737
  "pathPattern": "/sites/{site-id}/getByPath(path='{path}')",
@@ -770,18 +796,21 @@
770
796
  "pathPattern": "/groups/{group-id}/conversations",
771
797
  "method": "get",
772
798
  "toolName": "list-group-conversations",
773
- "workScopes": ["Group.Read.All"]
799
+ "workScopes": ["Group.Read.All"],
800
+ "llmTip": "Legacy — Microsoft recommends Teams channels instead of group conversations."
774
801
  },
775
802
  {
776
803
  "pathPattern": "/groups/{group-id}/threads",
777
804
  "method": "get",
778
805
  "toolName": "list-group-threads",
779
- "workScopes": ["Group.Read.All"]
806
+ "workScopes": ["Group.Read.All"],
807
+ "llmTip": "Legacy — Microsoft recommends Teams channels instead of group threads."
780
808
  },
781
809
  {
782
810
  "pathPattern": "/groups/{group-id}/threads/{conversationThread-id}/reply",
783
811
  "method": "post",
784
812
  "toolName": "reply-to-group-thread",
785
- "workScopes": ["Group.ReadWrite.All"]
813
+ "workScopes": ["Group.ReadWrite.All"],
814
+ "llmTip": "Legacy — Microsoft recommends Teams channels instead of group threads."
786
815
  }
787
816
  ]
@@ -62,15 +62,16 @@ async function executeGraphTool(tool, config, graphClient, params, authManager)
62
62
  const normalizedParamName = paramName.startsWith("$") ? paramName.slice(1) : paramName;
63
63
  const isOdataParam = odataParams.includes(normalizedParamName.toLowerCase());
64
64
  const fixedParamName = isOdataParam ? `$${normalizedParamName.toLowerCase()}` : paramName;
65
+ const camelCaseParamName = paramName.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
65
66
  const paramDef = parameterDefinitions.find(
66
- (p) => p.name === paramName || isOdataParam && p.name === normalizedParamName
67
+ (p) => p.name === paramName || p.name === camelCaseParamName || isOdataParam && p.name === normalizedParamName
67
68
  );
68
69
  if (paramDef) {
69
70
  switch (paramDef.type) {
70
71
  case "Path": {
71
72
  const shouldSkipEncoding = config?.skipEncoding?.includes(paramName) ?? false;
72
73
  const encodedValue = shouldSkipEncoding ? paramValue : encodeURIComponent(paramValue).replace(/%3D/g, "=");
73
- path2 = path2.replace(`{${paramName}}`, encodedValue).replace(`:${paramName}`, encodedValue);
74
+ path2 = path2.replace(`{${paramName}}`, encodedValue).replace(`:${paramName}`, encodedValue).replace(`{${camelCaseParamName}}`, encodedValue).replace(`:${camelCaseParamName}`, encodedValue);
74
75
  break;
75
76
  }
76
77
  case "Query":
@@ -106,6 +107,10 @@ async function executeGraphTool(tool, config, graphClient, params, authManager)
106
107
  } else if (paramName === "body") {
107
108
  body = paramValue;
108
109
  logger.info(`Set body param: ${JSON.stringify(body)}`);
110
+ } else if (path2.includes(`:${paramName}`) || path2.includes(`{${paramName}}`) || path2.includes(`:${camelCaseParamName}`) || path2.includes(`{${camelCaseParamName}}`)) {
111
+ const encodedValue = encodeURIComponent(paramValue).replace(/%3D/g, "=");
112
+ path2 = path2.replace(`{${paramName}}`, encodedValue).replace(`:${paramName}`, encodedValue).replace(`{${camelCaseParamName}}`, encodedValue).replace(`:${camelCaseParamName}`, encodedValue);
113
+ logger.info(`Path param fallback: replaced :${camelCaseParamName} with encoded value`);
109
114
  }
110
115
  }
111
116
  const preferValues = [];
@@ -138,7 +143,7 @@ async function executeGraphTool(tool, config, graphClient, params, authManager)
138
143
  logger.info(`Setting custom Accept: ${config.acceptType}`);
139
144
  }
140
145
  if (Object.keys(queryParams).length > 0) {
141
- const queryString = Object.entries(queryParams).map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`).join("&");
146
+ const queryString = Object.entries(queryParams).map(([key, value]) => `${key}=${encodeURIComponent(value).replace(/%2C/gi, ",")}`).join("&");
142
147
  path2 = `${path2}${path2.includes("?") ? "&" : "?"}${queryString}`;
143
148
  }
144
149
  const options = {
@@ -300,9 +305,48 @@ function registerGraphTools(server, graphClient, readOnly = false, enabledToolsP
300
305
  paramSchema[param.name] = param.schema || z.any();
301
306
  }
302
307
  }
308
+ const pathParamMatches = tool.path.matchAll(/:([a-zA-Z]+)/g);
309
+ for (const match of pathParamMatches) {
310
+ const pathParamName = match[1];
311
+ if (!(pathParamName in paramSchema)) {
312
+ paramSchema[pathParamName] = z.string().describe(`Path parameter: ${pathParamName}`);
313
+ }
314
+ }
303
315
  if (tool.method.toUpperCase() === "GET" && tool.path.includes("/")) {
304
316
  paramSchema["fetchAllPages"] = z.boolean().describe("Automatically fetch all pages of results").optional();
305
317
  }
318
+ if (paramSchema["filter"] !== void 0 || paramSchema["$filter"] !== void 0) {
319
+ const key = paramSchema["$filter"] !== void 0 ? "$filter" : "filter";
320
+ paramSchema[key] = z.string().describe(
321
+ "OData filter expression. Add $count=true for advanced filters (flag/flagStatus, contains()). Cannot combine with $search."
322
+ ).optional();
323
+ }
324
+ if (paramSchema["search"] !== void 0 || paramSchema["$search"] !== void 0) {
325
+ const key = paramSchema["$search"] !== void 0 ? "$search" : "search";
326
+ paramSchema[key] = z.string().describe("KQL search query \u2014 wrap value in double quotes. Cannot combine with $filter.").optional();
327
+ }
328
+ if (paramSchema["select"] !== void 0 || paramSchema["$select"] !== void 0) {
329
+ const key = paramSchema["$select"] !== void 0 ? "$select" : "select";
330
+ paramSchema[key] = z.string().describe("Comma-separated fields to return, e.g. id,subject,from,receivedDateTime").optional();
331
+ }
332
+ if (paramSchema["orderby"] !== void 0 || paramSchema["$orderby"] !== void 0) {
333
+ const key = paramSchema["$orderby"] !== void 0 ? "$orderby" : "orderby";
334
+ paramSchema[key] = z.string().describe("Sort expression, e.g. receivedDateTime desc").optional();
335
+ }
336
+ if (paramSchema["top"] !== void 0 || paramSchema["$top"] !== void 0) {
337
+ const key = paramSchema["$top"] !== void 0 ? "$top" : "top";
338
+ paramSchema[key] = z.number().describe("Max items per page").optional();
339
+ }
340
+ if (paramSchema["skip"] !== void 0 || paramSchema["$skip"] !== void 0) {
341
+ const key = paramSchema["$skip"] !== void 0 ? "$skip" : "skip";
342
+ paramSchema[key] = z.number().describe("Items to skip for pagination. Not supported with $search.").optional();
343
+ }
344
+ if (paramSchema["count"] !== void 0 || paramSchema["$count"] !== void 0) {
345
+ const countKey = paramSchema["$count"] !== void 0 ? "$count" : "count";
346
+ paramSchema[countKey] = z.boolean().describe(
347
+ "Set true to enable advanced query mode (ConsistencyLevel: eventual). Required for complex $filter on flag/flagStatus or contains()."
348
+ ).optional();
349
+ }
306
350
  if (multiAccount) {
307
351
  const accountHint = accountNames.length > 0 ? `Known accounts: ${accountNames.join(", ")}. ` : "";
308
352
  paramSchema["account"] = z.string().describe(
@@ -1,10 +1,10 @@
1
- 2026-03-23 22:15:31 INFO: [GRAPH CLIENT] Final URL being sent to Microsoft: https://graph.microsoft.com/v1.0/me
2
- 2026-03-23 22:15:31 INFO: [GRAPH CLIENT] Final URL being sent to Microsoft: https://graph.microsoft.com/v1.0/me
3
- 2026-03-23 22:15:31 INFO: [GRAPH CLIENT] Final URL being sent to Microsoft: https://graph.microsoft.com/v1.0/me
4
- 2026-03-23 22:15:31 INFO: [GRAPH CLIENT] Final URL being sent to Microsoft: https://graph.microsoft.com/v1.0/me/messages
5
- 2026-03-23 22:15:31 INFO: [GRAPH CLIENT] Final URL being sent to Microsoft: https://graph.microsoft.com/v1.0/me/calendar
6
- 2026-03-23 22:15:32 INFO: Using environment variables for secrets
7
- 2026-03-23 22:15:32 INFO: Using environment variables for secrets
8
- 2026-03-23 22:15:32 INFO: Using environment variables for secrets
9
- 2026-03-23 22:15:32 INFO: Using environment variables for secrets
10
- 2026-03-23 22:15:32 INFO: Using environment variables for secrets
1
+ 2026-03-25 11:58:50 INFO: [GRAPH CLIENT] Final URL being sent to Microsoft: https://graph.microsoft.com/v1.0/me
2
+ 2026-03-25 11:58:50 INFO: [GRAPH CLIENT] Final URL being sent to Microsoft: https://graph.microsoft.com/v1.0/me
3
+ 2026-03-25 11:58:50 INFO: [GRAPH CLIENT] Final URL being sent to Microsoft: https://graph.microsoft.com/v1.0/me
4
+ 2026-03-25 11:58:50 INFO: [GRAPH CLIENT] Final URL being sent to Microsoft: https://graph.microsoft.com/v1.0/me/messages
5
+ 2026-03-25 11:58:50 INFO: [GRAPH CLIENT] Final URL being sent to Microsoft: https://graph.microsoft.com/v1.0/me/calendar
6
+ 2026-03-25 11:58:51 INFO: Using environment variables for secrets
7
+ 2026-03-25 11:58:51 INFO: Using environment variables for secrets
8
+ 2026-03-25 11:58:51 INFO: Using environment variables for secrets
9
+ 2026-03-25 11:58:51 INFO: Using environment variables for secrets
10
+ 2026-03-25 11:58:51 INFO: Using environment variables for secrets
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@softeria/ms-365-mcp-server",
3
- "version": "0.46.1",
3
+ "version": "0.46.2",
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",
@@ -110,13 +110,15 @@
110
110
  "pathPattern": "/me/messages/{message-id}",
111
111
  "method": "delete",
112
112
  "toolName": "delete-mail-message",
113
- "scopes": ["Mail.ReadWrite"]
113
+ "scopes": ["Mail.ReadWrite"],
114
+ "llmTip": "Soft delete — moves to Deleted Items. To permanently delete, delete again from Deleted Items."
114
115
  },
115
116
  {
116
117
  "pathPattern": "/me/messages/{message-id}/move",
117
118
  "method": "post",
118
119
  "toolName": "move-mail-message",
119
- "scopes": ["Mail.ReadWrite"]
120
+ "scopes": ["Mail.ReadWrite"],
121
+ "llmTip": "destinationId accepts folder ID or well-known name (inbox, drafts, sentitems, deleteditems, junkemail, archive)."
120
122
  },
121
123
  {
122
124
  "pathPattern": "/me/messages/{message-id}",
@@ -128,7 +130,8 @@
128
130
  "pathPattern": "/me/messages/{message-id}/attachments",
129
131
  "method": "post",
130
132
  "toolName": "add-mail-attachment",
131
- "scopes": ["Mail.ReadWrite"]
133
+ "scopes": ["Mail.ReadWrite"],
134
+ "llmTip": "Max 3MB. Body requires @odata.type: {\"@odata.type\": \"#microsoft.graph.fileAttachment\", \"name\": \"file.pdf\", \"contentBytes\": \"<base64>\"}."
132
135
  },
133
136
  {
134
137
  "pathPattern": "/me/messages/{message-id}/attachments",
@@ -192,7 +195,8 @@
192
195
  "pathPattern": "/me/messages/{message-id}/send",
193
196
  "method": "post",
194
197
  "toolName": "send-draft-message",
195
- "scopes": ["Mail.Send"]
198
+ "scopes": ["Mail.Send"],
199
+ "llmTip": "No request body needed — just call with the message ID. Draft must exist in Drafts folder."
196
200
  },
197
201
  {
198
202
  "pathPattern": "/me/events",
@@ -200,7 +204,8 @@
200
204
  "toolName": "list-calendar-events",
201
205
  "scopes": ["Calendars.Read"],
202
206
  "supportsTimezone": true,
203
- "supportsExpandExtendedProperties": true
207
+ "supportsExpandExtendedProperties": true,
208
+ "llmTip": "WARNING: Does NOT expand recurring events — only returns seriesMaster. Use get-calendar-view instead to see individual occurrences within a date range."
204
209
  },
205
210
  {
206
211
  "pathPattern": "/me/events/{event-id}",
@@ -222,13 +227,14 @@
222
227
  "method": "patch",
223
228
  "toolName": "update-calendar-event",
224
229
  "scopes": ["Calendars.ReadWrite"],
225
- "llmTip": "CRITICAL: Do not try to guess the email address of the recipients. Use the list-users tool to find the email address of the recipients."
230
+ "llmTip": "CRITICAL: Do not try to guess the email address of the recipients. Use the list-users tool to find the email address of the recipients. WARNING: Setting attendees replaces the entire attendee list — include all attendees, not just new ones."
226
231
  },
227
232
  {
228
233
  "pathPattern": "/me/events/{event-id}",
229
234
  "method": "delete",
230
235
  "toolName": "delete-calendar-event",
231
- "scopes": ["Calendars.ReadWrite"]
236
+ "scopes": ["Calendars.ReadWrite"],
237
+ "llmTip": "Deleting a seriesMaster deletes ALL occurrences of the recurring event. To cancel a single occurrence, delete that specific instance ID from list-calendar-event-instances."
232
238
  },
233
239
  {
234
240
  "pathPattern": "/me/calendars/{calendar-id}/events",
@@ -236,7 +242,8 @@
236
242
  "toolName": "list-specific-calendar-events",
237
243
  "scopes": ["Calendars.Read"],
238
244
  "supportsTimezone": true,
239
- "supportsExpandExtendedProperties": true
245
+ "supportsExpandExtendedProperties": true,
246
+ "llmTip": "WARNING: Does NOT expand recurring events — only returns seriesMaster. Use get-specific-calendar-view instead."
240
247
  },
241
248
  {
242
249
  "pathPattern": "/me/calendars/{calendar-id}/events/{event-id}",
@@ -258,13 +265,14 @@
258
265
  "method": "patch",
259
266
  "toolName": "update-specific-calendar-event",
260
267
  "scopes": ["Calendars.ReadWrite"],
261
- "llmTip": "CRITICAL: Do not try to guess the email address of the recipients. Use the list-users tool to find the email address of the recipients."
268
+ "llmTip": "CRITICAL: Do not try to guess the email address of the recipients. Use the list-users tool to find the email address of the recipients. WARNING: Setting attendees replaces the entire attendee list — include all attendees, not just new ones."
262
269
  },
263
270
  {
264
271
  "pathPattern": "/me/calendars/{calendar-id}/events/{event-id}",
265
272
  "method": "delete",
266
273
  "toolName": "delete-specific-calendar-event",
267
- "scopes": ["Calendars.ReadWrite"]
274
+ "scopes": ["Calendars.ReadWrite"],
275
+ "llmTip": "Deleting a seriesMaster deletes ALL occurrences. To cancel a single occurrence, use the specific instance ID."
268
276
  },
269
277
  {
270
278
  "pathPattern": "/me/calendarView",
@@ -334,7 +342,8 @@
334
342
  "method": "get",
335
343
  "toolName": "download-onedrive-file-content",
336
344
  "scopes": ["Files.Read"],
337
- "returnDownloadUrl": true
345
+ "returnDownloadUrl": true,
346
+ "llmTip": "Returns a temporary download URL, NOT the file content directly."
338
347
  },
339
348
  {
340
349
  "pathPattern": "/drives/{drive-id}/items/{driveItem-id}",
@@ -346,7 +355,8 @@
346
355
  "pathPattern": "/drives/{drive-id}/items/{driveItem-id}/content",
347
356
  "method": "put",
348
357
  "toolName": "upload-file-content",
349
- "scopes": ["Files.ReadWrite"]
358
+ "scopes": ["Files.ReadWrite"],
359
+ "llmTip": "Max 4MB. For new files use path format: /items/root:/path/to/file.txt:/content. Overwrites existing files without warning."
350
360
  },
351
361
  {
352
362
  "pathPattern": "/drives/{drive-id}/items/{driveItem-id}/workbook/worksheets/{workbookWorksheet-id}/charts/add",
@@ -413,14 +423,16 @@
413
423
  "method": "post",
414
424
  "toolName": "create-onenote-page",
415
425
  "scopes": ["Notes.Create"],
416
- "contentType": "text/html"
426
+ "contentType": "text/html",
427
+ "llmTip": "Body must be a full HTML document (with <html><head><title>...</title></head><body>...</body></html>). Partial HTML or plain text fails silently or creates malformed pages."
417
428
  },
418
429
  {
419
430
  "pathPattern": "/me/onenote/sections/{onenoteSection-id}/pages",
420
431
  "method": "post",
421
432
  "toolName": "create-onenote-section-page",
422
433
  "scopes": ["Notes.Create"],
423
- "contentType": "text/html"
434
+ "contentType": "text/html",
435
+ "llmTip": "Body must be a full HTML document (with <html><head><title>...</title></head><body>...</body></html>). Partial HTML fails silently."
424
436
  },
425
437
  {
426
438
  "pathPattern": "/me/todo/lists",
@@ -465,7 +477,8 @@
465
477
  "pathPattern": "/me/planner/tasks",
466
478
  "method": "get",
467
479
  "toolName": "list-planner-tasks",
468
- "scopes": ["Tasks.Read"]
480
+ "scopes": ["Tasks.Read"],
481
+ "llmTip": "Priority values: 0=Urgent, 1=Important, 3=Medium, 5=Low, 9=unset."
469
482
  },
470
483
  {
471
484
  "pathPattern": "/planner/plans/{plannerPlan-id}",
@@ -477,13 +490,15 @@
477
490
  "pathPattern": "/planner/plans/{plannerPlan-id}/tasks",
478
491
  "method": "get",
479
492
  "toolName": "list-plan-tasks",
480
- "scopes": ["Tasks.Read"]
493
+ "scopes": ["Tasks.Read"],
494
+ "llmTip": "Priority values: 0=Urgent, 1=Important, 3=Medium, 5=Low, 9=unset."
481
495
  },
482
496
  {
483
497
  "pathPattern": "/planner/tasks/{plannerTask-id}",
484
498
  "method": "get",
485
499
  "toolName": "get-planner-task",
486
- "scopes": ["Tasks.Read"]
500
+ "scopes": ["Tasks.Read"],
501
+ "llmTip": "Response includes @odata.etag — save it, required as If-Match header for update-planner-task. Use includeHeaders=true to capture it."
487
502
  },
488
503
  {
489
504
  "pathPattern": "/planner/tasks",
@@ -495,25 +510,29 @@
495
510
  "pathPattern": "/planner/tasks/{plannerTask-id}",
496
511
  "method": "patch",
497
512
  "toolName": "update-planner-task",
498
- "scopes": ["Tasks.ReadWrite"]
513
+ "scopes": ["Tasks.ReadWrite"],
514
+ "llmTip": "CRITICAL: Requires If-Match header with the task's @odata.etag value, otherwise returns 412 Precondition Failed. Get the ETag from get-planner-task with includeHeaders=true. Priority values: 0=Urgent, 1=Important, 3=Medium, 5=Low, 9=unset."
499
515
  },
500
516
  {
501
517
  "pathPattern": "/planner/tasks/{plannerTask-id}/details",
502
518
  "method": "get",
503
519
  "toolName": "get-planner-task-details",
504
- "scopes": ["Tasks.Read"]
520
+ "scopes": ["Tasks.Read"],
521
+ "llmTip": "Response includes @odata.etag — required for update-planner-task-details. Use includeHeaders=true."
505
522
  },
506
523
  {
507
524
  "pathPattern": "/planner/tasks/{plannerTask-id}/details",
508
525
  "method": "patch",
509
526
  "toolName": "update-planner-task-details",
510
- "scopes": ["Tasks.ReadWrite"]
527
+ "scopes": ["Tasks.ReadWrite"],
528
+ "llmTip": "CRITICAL: Requires If-Match header with ETag from get-planner-task-details (use includeHeaders=true). Checklist items use GUID keys: {\"checklist\": {\"<guid>\": {\"title\": \"...\", \"isChecked\": false}}}."
511
529
  },
512
530
  {
513
531
  "pathPattern": "/me/contacts",
514
532
  "method": "get",
515
533
  "toolName": "list-outlook-contacts",
516
- "scopes": ["Contacts.Read"]
534
+ "scopes": ["Contacts.Read"],
535
+ "llmTip": "$filter only supports startswith() — contains() and eq on emailAddresses do not work. Use $search as alternative for broader matching."
517
536
  },
518
537
  {
519
538
  "pathPattern": "/me/contacts/{contact-id}",
@@ -531,7 +550,8 @@
531
550
  "pathPattern": "/me/contacts/{contact-id}",
532
551
  "method": "patch",
533
552
  "toolName": "update-outlook-contact",
534
- "scopes": ["Contacts.ReadWrite"]
553
+ "scopes": ["Contacts.ReadWrite"],
554
+ "llmTip": "emailAddresses array is replaced entirely — include all addresses, not just new ones."
535
555
  },
536
556
  {
537
557
  "pathPattern": "/me/contacts/{contact-id}",
@@ -573,7 +593,8 @@
573
593
  "pathPattern": "/chats/{chat-id}/messages",
574
594
  "method": "post",
575
595
  "toolName": "send-chat-message",
576
- "workScopes": ["ChatMessage.Send"]
596
+ "workScopes": ["ChatMessage.Send"],
597
+ "llmTip": "Use contentType 'html' in the body — plain text contentType gets mangled by Graph API."
577
598
  },
578
599
  {
579
600
  "pathPattern": "/me/joinedTeams",
@@ -615,13 +636,15 @@
615
636
  "pathPattern": "/teams/{team-id}/channels/{channel-id}/messages",
616
637
  "method": "post",
617
638
  "toolName": "send-channel-message",
618
- "workScopes": ["ChannelMessage.Send"]
639
+ "workScopes": ["ChannelMessage.Send"],
640
+ "llmTip": "Use contentType 'html' in the body — plain text contentType gets mangled by Graph API."
619
641
  },
620
642
  {
621
643
  "pathPattern": "/teams/{team-id}/channels/{channel-id}/messages/{chatMessage-id}/replies",
622
644
  "method": "post",
623
645
  "toolName": "reply-to-channel-message",
624
- "workScopes": ["ChannelMessage.Send"]
646
+ "workScopes": ["ChannelMessage.Send"],
647
+ "llmTip": "Use contentType 'html' in the body — plain text contentType gets mangled by Graph API."
625
648
  },
626
649
  {
627
650
  "pathPattern": "/teams/{team-id}/channels/{channel-id}/messages/{chatMessage-id}/replies",
@@ -645,7 +668,8 @@
645
668
  "pathPattern": "/chats/{chat-id}/messages/{chatMessage-id}/replies",
646
669
  "method": "post",
647
670
  "toolName": "reply-to-chat-message",
648
- "workScopes": ["ChatMessage.Send"]
671
+ "workScopes": ["ChatMessage.Send"],
672
+ "llmTip": "Use contentType 'html' in the body — plain text contentType gets mangled by Graph API."
649
673
  },
650
674
  {
651
675
  "pathPattern": "/sites",
@@ -699,13 +723,15 @@
699
723
  "pathPattern": "/sites/{site-id}/lists/{list-id}/items",
700
724
  "method": "get",
701
725
  "toolName": "list-sharepoint-site-list-items",
702
- "workScopes": ["Sites.Read.All"]
726
+ "workScopes": ["Sites.Read.All"],
727
+ "llmTip": "Add $expand=fields to include actual column values. Without it, only metadata is returned."
703
728
  },
704
729
  {
705
730
  "pathPattern": "/sites/{site-id}/lists/{list-id}/items/{listItem-id}",
706
731
  "method": "get",
707
732
  "toolName": "get-sharepoint-site-list-item",
708
- "workScopes": ["Sites.Read.All"]
733
+ "workScopes": ["Sites.Read.All"],
734
+ "llmTip": "Add $expand=fields to include actual column values. Without it, only metadata is returned."
709
735
  },
710
736
  {
711
737
  "pathPattern": "/sites/{site-id}/getByPath(path='{path}')",
@@ -770,18 +796,21 @@
770
796
  "pathPattern": "/groups/{group-id}/conversations",
771
797
  "method": "get",
772
798
  "toolName": "list-group-conversations",
773
- "workScopes": ["Group.Read.All"]
799
+ "workScopes": ["Group.Read.All"],
800
+ "llmTip": "Legacy — Microsoft recommends Teams channels instead of group conversations."
774
801
  },
775
802
  {
776
803
  "pathPattern": "/groups/{group-id}/threads",
777
804
  "method": "get",
778
805
  "toolName": "list-group-threads",
779
- "workScopes": ["Group.Read.All"]
806
+ "workScopes": ["Group.Read.All"],
807
+ "llmTip": "Legacy — Microsoft recommends Teams channels instead of group threads."
780
808
  },
781
809
  {
782
810
  "pathPattern": "/groups/{group-id}/threads/{conversationThread-id}/reply",
783
811
  "method": "post",
784
812
  "toolName": "reply-to-group-thread",
785
- "workScopes": ["Group.ReadWrite.All"]
813
+ "workScopes": ["Group.ReadWrite.All"],
814
+ "llmTip": "Legacy — Microsoft recommends Teams channels instead of group threads."
786
815
  }
787
816
  ]
package/tsup.config.ts CHANGED
@@ -11,7 +11,7 @@ export default defineConfig({
11
11
  sourcemap: false,
12
12
  dts: false,
13
13
  publicDir: false,
14
- onSuccess: 'chmod +x dist/index.js',
14
+ onSuccess: process.platform === 'win32' ? undefined : 'chmod +x dist/index.js',
15
15
  loader: {
16
16
  '.json': 'copy',
17
17
  },