@softeria/ms-365-mcp-server 0.46.0 → 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.
- package/dist/__tests__/graph-tools.test.js +355 -0
- package/dist/endpoints.json +66 -34
- package/dist/graph-tools.js +47 -3
- package/logs/mcp-server.log +10 -10
- package/package.json +1 -1
- package/src/endpoints.json +66 -34
- package/tsup.config.ts +1 -1
|
@@ -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
|
+
});
|
package/dist/endpoints.json
CHANGED
|
@@ -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,32 +423,37 @@
|
|
|
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",
|
|
427
439
|
"method": "get",
|
|
428
440
|
"toolName": "list-todo-task-lists",
|
|
429
|
-
"scopes": ["Tasks.Read"]
|
|
441
|
+
"scopes": ["Tasks.Read"],
|
|
442
|
+
"llmTip": "Lists all To Do task lists. Returns todoTaskList-id needed for all task operations. The default list is typically called 'Tasks'. NOTE: $select is NOT supported by this endpoint — do not pass select parameter, Graph returns 400."
|
|
430
443
|
},
|
|
431
444
|
{
|
|
432
445
|
"pathPattern": "/me/todo/lists/{todoTaskList-id}/tasks",
|
|
433
446
|
"method": "get",
|
|
434
447
|
"toolName": "list-todo-tasks",
|
|
435
|
-
"scopes": ["Tasks.Read"]
|
|
448
|
+
"scopes": ["Tasks.Read"],
|
|
449
|
+
"llmTip": "Lists tasks in a To Do list. Requires todoTaskList-id — use list-todo-task-lists to find it. NOTE: $select is NOT supported — do not pass select, Graph returns 400. Use $filter=status eq 'notStarted' or $filter=status eq 'completed' to filter by status. Use $top to limit results. Status values: 'notStarted', 'inProgress', 'completed', 'waitingOnOthers', 'deferred'."
|
|
436
450
|
},
|
|
437
451
|
{
|
|
438
452
|
"pathPattern": "/me/todo/lists/{todoTaskList-id}/tasks/{todoTask-id}",
|
|
439
453
|
"method": "get",
|
|
440
454
|
"toolName": "get-todo-task",
|
|
441
|
-
"scopes": ["Tasks.Read"]
|
|
455
|
+
"scopes": ["Tasks.Read"],
|
|
456
|
+
"llmTip": "Returns a single To Do task. NOTE: $select is NOT supported — do not pass select parameter, Graph returns RequestBroker--ParseUri (400). Use $expand=linkedResources to include linked email/resource. Returns body content (HTML format), checklist items, and linked resources."
|
|
442
457
|
},
|
|
443
458
|
{
|
|
444
459
|
"pathPattern": "/me/todo/lists/{todoTaskList-id}/tasks",
|
|
@@ -462,7 +477,8 @@
|
|
|
462
477
|
"pathPattern": "/me/planner/tasks",
|
|
463
478
|
"method": "get",
|
|
464
479
|
"toolName": "list-planner-tasks",
|
|
465
|
-
"scopes": ["Tasks.Read"]
|
|
480
|
+
"scopes": ["Tasks.Read"],
|
|
481
|
+
"llmTip": "Priority values: 0=Urgent, 1=Important, 3=Medium, 5=Low, 9=unset."
|
|
466
482
|
},
|
|
467
483
|
{
|
|
468
484
|
"pathPattern": "/planner/plans/{plannerPlan-id}",
|
|
@@ -474,13 +490,15 @@
|
|
|
474
490
|
"pathPattern": "/planner/plans/{plannerPlan-id}/tasks",
|
|
475
491
|
"method": "get",
|
|
476
492
|
"toolName": "list-plan-tasks",
|
|
477
|
-
"scopes": ["Tasks.Read"]
|
|
493
|
+
"scopes": ["Tasks.Read"],
|
|
494
|
+
"llmTip": "Priority values: 0=Urgent, 1=Important, 3=Medium, 5=Low, 9=unset."
|
|
478
495
|
},
|
|
479
496
|
{
|
|
480
497
|
"pathPattern": "/planner/tasks/{plannerTask-id}",
|
|
481
498
|
"method": "get",
|
|
482
499
|
"toolName": "get-planner-task",
|
|
483
|
-
"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."
|
|
484
502
|
},
|
|
485
503
|
{
|
|
486
504
|
"pathPattern": "/planner/tasks",
|
|
@@ -492,25 +510,29 @@
|
|
|
492
510
|
"pathPattern": "/planner/tasks/{plannerTask-id}",
|
|
493
511
|
"method": "patch",
|
|
494
512
|
"toolName": "update-planner-task",
|
|
495
|
-
"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."
|
|
496
515
|
},
|
|
497
516
|
{
|
|
498
517
|
"pathPattern": "/planner/tasks/{plannerTask-id}/details",
|
|
499
518
|
"method": "get",
|
|
500
519
|
"toolName": "get-planner-task-details",
|
|
501
|
-
"scopes": ["Tasks.Read"]
|
|
520
|
+
"scopes": ["Tasks.Read"],
|
|
521
|
+
"llmTip": "Response includes @odata.etag — required for update-planner-task-details. Use includeHeaders=true."
|
|
502
522
|
},
|
|
503
523
|
{
|
|
504
524
|
"pathPattern": "/planner/tasks/{plannerTask-id}/details",
|
|
505
525
|
"method": "patch",
|
|
506
526
|
"toolName": "update-planner-task-details",
|
|
507
|
-
"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}}}."
|
|
508
529
|
},
|
|
509
530
|
{
|
|
510
531
|
"pathPattern": "/me/contacts",
|
|
511
532
|
"method": "get",
|
|
512
533
|
"toolName": "list-outlook-contacts",
|
|
513
|
-
"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."
|
|
514
536
|
},
|
|
515
537
|
{
|
|
516
538
|
"pathPattern": "/me/contacts/{contact-id}",
|
|
@@ -528,7 +550,8 @@
|
|
|
528
550
|
"pathPattern": "/me/contacts/{contact-id}",
|
|
529
551
|
"method": "patch",
|
|
530
552
|
"toolName": "update-outlook-contact",
|
|
531
|
-
"scopes": ["Contacts.ReadWrite"]
|
|
553
|
+
"scopes": ["Contacts.ReadWrite"],
|
|
554
|
+
"llmTip": "emailAddresses array is replaced entirely — include all addresses, not just new ones."
|
|
532
555
|
},
|
|
533
556
|
{
|
|
534
557
|
"pathPattern": "/me/contacts/{contact-id}",
|
|
@@ -570,7 +593,8 @@
|
|
|
570
593
|
"pathPattern": "/chats/{chat-id}/messages",
|
|
571
594
|
"method": "post",
|
|
572
595
|
"toolName": "send-chat-message",
|
|
573
|
-
"workScopes": ["ChatMessage.Send"]
|
|
596
|
+
"workScopes": ["ChatMessage.Send"],
|
|
597
|
+
"llmTip": "Use contentType 'html' in the body — plain text contentType gets mangled by Graph API."
|
|
574
598
|
},
|
|
575
599
|
{
|
|
576
600
|
"pathPattern": "/me/joinedTeams",
|
|
@@ -612,13 +636,15 @@
|
|
|
612
636
|
"pathPattern": "/teams/{team-id}/channels/{channel-id}/messages",
|
|
613
637
|
"method": "post",
|
|
614
638
|
"toolName": "send-channel-message",
|
|
615
|
-
"workScopes": ["ChannelMessage.Send"]
|
|
639
|
+
"workScopes": ["ChannelMessage.Send"],
|
|
640
|
+
"llmTip": "Use contentType 'html' in the body — plain text contentType gets mangled by Graph API."
|
|
616
641
|
},
|
|
617
642
|
{
|
|
618
643
|
"pathPattern": "/teams/{team-id}/channels/{channel-id}/messages/{chatMessage-id}/replies",
|
|
619
644
|
"method": "post",
|
|
620
645
|
"toolName": "reply-to-channel-message",
|
|
621
|
-
"workScopes": ["ChannelMessage.Send"]
|
|
646
|
+
"workScopes": ["ChannelMessage.Send"],
|
|
647
|
+
"llmTip": "Use contentType 'html' in the body — plain text contentType gets mangled by Graph API."
|
|
622
648
|
},
|
|
623
649
|
{
|
|
624
650
|
"pathPattern": "/teams/{team-id}/channels/{channel-id}/messages/{chatMessage-id}/replies",
|
|
@@ -642,7 +668,8 @@
|
|
|
642
668
|
"pathPattern": "/chats/{chat-id}/messages/{chatMessage-id}/replies",
|
|
643
669
|
"method": "post",
|
|
644
670
|
"toolName": "reply-to-chat-message",
|
|
645
|
-
"workScopes": ["ChatMessage.Send"]
|
|
671
|
+
"workScopes": ["ChatMessage.Send"],
|
|
672
|
+
"llmTip": "Use contentType 'html' in the body — plain text contentType gets mangled by Graph API."
|
|
646
673
|
},
|
|
647
674
|
{
|
|
648
675
|
"pathPattern": "/sites",
|
|
@@ -696,13 +723,15 @@
|
|
|
696
723
|
"pathPattern": "/sites/{site-id}/lists/{list-id}/items",
|
|
697
724
|
"method": "get",
|
|
698
725
|
"toolName": "list-sharepoint-site-list-items",
|
|
699
|
-
"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."
|
|
700
728
|
},
|
|
701
729
|
{
|
|
702
730
|
"pathPattern": "/sites/{site-id}/lists/{list-id}/items/{listItem-id}",
|
|
703
731
|
"method": "get",
|
|
704
732
|
"toolName": "get-sharepoint-site-list-item",
|
|
705
|
-
"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."
|
|
706
735
|
},
|
|
707
736
|
{
|
|
708
737
|
"pathPattern": "/sites/{site-id}/getByPath(path='{path}')",
|
|
@@ -767,18 +796,21 @@
|
|
|
767
796
|
"pathPattern": "/groups/{group-id}/conversations",
|
|
768
797
|
"method": "get",
|
|
769
798
|
"toolName": "list-group-conversations",
|
|
770
|
-
"workScopes": ["Group.Read.All"]
|
|
799
|
+
"workScopes": ["Group.Read.All"],
|
|
800
|
+
"llmTip": "Legacy — Microsoft recommends Teams channels instead of group conversations."
|
|
771
801
|
},
|
|
772
802
|
{
|
|
773
803
|
"pathPattern": "/groups/{group-id}/threads",
|
|
774
804
|
"method": "get",
|
|
775
805
|
"toolName": "list-group-threads",
|
|
776
|
-
"workScopes": ["Group.Read.All"]
|
|
806
|
+
"workScopes": ["Group.Read.All"],
|
|
807
|
+
"llmTip": "Legacy — Microsoft recommends Teams channels instead of group threads."
|
|
777
808
|
},
|
|
778
809
|
{
|
|
779
810
|
"pathPattern": "/groups/{group-id}/threads/{conversationThread-id}/reply",
|
|
780
811
|
"method": "post",
|
|
781
812
|
"toolName": "reply-to-group-thread",
|
|
782
|
-
"workScopes": ["Group.ReadWrite.All"]
|
|
813
|
+
"workScopes": ["Group.ReadWrite.All"],
|
|
814
|
+
"llmTip": "Legacy — Microsoft recommends Teams channels instead of group threads."
|
|
783
815
|
}
|
|
784
816
|
]
|
package/dist/graph-tools.js
CHANGED
|
@@ -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]) => `${
|
|
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(
|
package/logs/mcp-server.log
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
2026-03-
|
|
2
|
-
2026-03-
|
|
3
|
-
2026-03-
|
|
4
|
-
2026-03-
|
|
5
|
-
2026-03-
|
|
6
|
-
2026-03-
|
|
7
|
-
2026-03-
|
|
8
|
-
2026-03-
|
|
9
|
-
2026-03-
|
|
10
|
-
2026-03-
|
|
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.
|
|
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",
|
package/src/endpoints.json
CHANGED
|
@@ -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,32 +423,37 @@
|
|
|
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",
|
|
427
439
|
"method": "get",
|
|
428
440
|
"toolName": "list-todo-task-lists",
|
|
429
|
-
"scopes": ["Tasks.Read"]
|
|
441
|
+
"scopes": ["Tasks.Read"],
|
|
442
|
+
"llmTip": "Lists all To Do task lists. Returns todoTaskList-id needed for all task operations. The default list is typically called 'Tasks'. NOTE: $select is NOT supported by this endpoint — do not pass select parameter, Graph returns 400."
|
|
430
443
|
},
|
|
431
444
|
{
|
|
432
445
|
"pathPattern": "/me/todo/lists/{todoTaskList-id}/tasks",
|
|
433
446
|
"method": "get",
|
|
434
447
|
"toolName": "list-todo-tasks",
|
|
435
|
-
"scopes": ["Tasks.Read"]
|
|
448
|
+
"scopes": ["Tasks.Read"],
|
|
449
|
+
"llmTip": "Lists tasks in a To Do list. Requires todoTaskList-id — use list-todo-task-lists to find it. NOTE: $select is NOT supported — do not pass select, Graph returns 400. Use $filter=status eq 'notStarted' or $filter=status eq 'completed' to filter by status. Use $top to limit results. Status values: 'notStarted', 'inProgress', 'completed', 'waitingOnOthers', 'deferred'."
|
|
436
450
|
},
|
|
437
451
|
{
|
|
438
452
|
"pathPattern": "/me/todo/lists/{todoTaskList-id}/tasks/{todoTask-id}",
|
|
439
453
|
"method": "get",
|
|
440
454
|
"toolName": "get-todo-task",
|
|
441
|
-
"scopes": ["Tasks.Read"]
|
|
455
|
+
"scopes": ["Tasks.Read"],
|
|
456
|
+
"llmTip": "Returns a single To Do task. NOTE: $select is NOT supported — do not pass select parameter, Graph returns RequestBroker--ParseUri (400). Use $expand=linkedResources to include linked email/resource. Returns body content (HTML format), checklist items, and linked resources."
|
|
442
457
|
},
|
|
443
458
|
{
|
|
444
459
|
"pathPattern": "/me/todo/lists/{todoTaskList-id}/tasks",
|
|
@@ -462,7 +477,8 @@
|
|
|
462
477
|
"pathPattern": "/me/planner/tasks",
|
|
463
478
|
"method": "get",
|
|
464
479
|
"toolName": "list-planner-tasks",
|
|
465
|
-
"scopes": ["Tasks.Read"]
|
|
480
|
+
"scopes": ["Tasks.Read"],
|
|
481
|
+
"llmTip": "Priority values: 0=Urgent, 1=Important, 3=Medium, 5=Low, 9=unset."
|
|
466
482
|
},
|
|
467
483
|
{
|
|
468
484
|
"pathPattern": "/planner/plans/{plannerPlan-id}",
|
|
@@ -474,13 +490,15 @@
|
|
|
474
490
|
"pathPattern": "/planner/plans/{plannerPlan-id}/tasks",
|
|
475
491
|
"method": "get",
|
|
476
492
|
"toolName": "list-plan-tasks",
|
|
477
|
-
"scopes": ["Tasks.Read"]
|
|
493
|
+
"scopes": ["Tasks.Read"],
|
|
494
|
+
"llmTip": "Priority values: 0=Urgent, 1=Important, 3=Medium, 5=Low, 9=unset."
|
|
478
495
|
},
|
|
479
496
|
{
|
|
480
497
|
"pathPattern": "/planner/tasks/{plannerTask-id}",
|
|
481
498
|
"method": "get",
|
|
482
499
|
"toolName": "get-planner-task",
|
|
483
|
-
"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."
|
|
484
502
|
},
|
|
485
503
|
{
|
|
486
504
|
"pathPattern": "/planner/tasks",
|
|
@@ -492,25 +510,29 @@
|
|
|
492
510
|
"pathPattern": "/planner/tasks/{plannerTask-id}",
|
|
493
511
|
"method": "patch",
|
|
494
512
|
"toolName": "update-planner-task",
|
|
495
|
-
"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."
|
|
496
515
|
},
|
|
497
516
|
{
|
|
498
517
|
"pathPattern": "/planner/tasks/{plannerTask-id}/details",
|
|
499
518
|
"method": "get",
|
|
500
519
|
"toolName": "get-planner-task-details",
|
|
501
|
-
"scopes": ["Tasks.Read"]
|
|
520
|
+
"scopes": ["Tasks.Read"],
|
|
521
|
+
"llmTip": "Response includes @odata.etag — required for update-planner-task-details. Use includeHeaders=true."
|
|
502
522
|
},
|
|
503
523
|
{
|
|
504
524
|
"pathPattern": "/planner/tasks/{plannerTask-id}/details",
|
|
505
525
|
"method": "patch",
|
|
506
526
|
"toolName": "update-planner-task-details",
|
|
507
|
-
"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}}}."
|
|
508
529
|
},
|
|
509
530
|
{
|
|
510
531
|
"pathPattern": "/me/contacts",
|
|
511
532
|
"method": "get",
|
|
512
533
|
"toolName": "list-outlook-contacts",
|
|
513
|
-
"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."
|
|
514
536
|
},
|
|
515
537
|
{
|
|
516
538
|
"pathPattern": "/me/contacts/{contact-id}",
|
|
@@ -528,7 +550,8 @@
|
|
|
528
550
|
"pathPattern": "/me/contacts/{contact-id}",
|
|
529
551
|
"method": "patch",
|
|
530
552
|
"toolName": "update-outlook-contact",
|
|
531
|
-
"scopes": ["Contacts.ReadWrite"]
|
|
553
|
+
"scopes": ["Contacts.ReadWrite"],
|
|
554
|
+
"llmTip": "emailAddresses array is replaced entirely — include all addresses, not just new ones."
|
|
532
555
|
},
|
|
533
556
|
{
|
|
534
557
|
"pathPattern": "/me/contacts/{contact-id}",
|
|
@@ -570,7 +593,8 @@
|
|
|
570
593
|
"pathPattern": "/chats/{chat-id}/messages",
|
|
571
594
|
"method": "post",
|
|
572
595
|
"toolName": "send-chat-message",
|
|
573
|
-
"workScopes": ["ChatMessage.Send"]
|
|
596
|
+
"workScopes": ["ChatMessage.Send"],
|
|
597
|
+
"llmTip": "Use contentType 'html' in the body — plain text contentType gets mangled by Graph API."
|
|
574
598
|
},
|
|
575
599
|
{
|
|
576
600
|
"pathPattern": "/me/joinedTeams",
|
|
@@ -612,13 +636,15 @@
|
|
|
612
636
|
"pathPattern": "/teams/{team-id}/channels/{channel-id}/messages",
|
|
613
637
|
"method": "post",
|
|
614
638
|
"toolName": "send-channel-message",
|
|
615
|
-
"workScopes": ["ChannelMessage.Send"]
|
|
639
|
+
"workScopes": ["ChannelMessage.Send"],
|
|
640
|
+
"llmTip": "Use contentType 'html' in the body — plain text contentType gets mangled by Graph API."
|
|
616
641
|
},
|
|
617
642
|
{
|
|
618
643
|
"pathPattern": "/teams/{team-id}/channels/{channel-id}/messages/{chatMessage-id}/replies",
|
|
619
644
|
"method": "post",
|
|
620
645
|
"toolName": "reply-to-channel-message",
|
|
621
|
-
"workScopes": ["ChannelMessage.Send"]
|
|
646
|
+
"workScopes": ["ChannelMessage.Send"],
|
|
647
|
+
"llmTip": "Use contentType 'html' in the body — plain text contentType gets mangled by Graph API."
|
|
622
648
|
},
|
|
623
649
|
{
|
|
624
650
|
"pathPattern": "/teams/{team-id}/channels/{channel-id}/messages/{chatMessage-id}/replies",
|
|
@@ -642,7 +668,8 @@
|
|
|
642
668
|
"pathPattern": "/chats/{chat-id}/messages/{chatMessage-id}/replies",
|
|
643
669
|
"method": "post",
|
|
644
670
|
"toolName": "reply-to-chat-message",
|
|
645
|
-
"workScopes": ["ChatMessage.Send"]
|
|
671
|
+
"workScopes": ["ChatMessage.Send"],
|
|
672
|
+
"llmTip": "Use contentType 'html' in the body — plain text contentType gets mangled by Graph API."
|
|
646
673
|
},
|
|
647
674
|
{
|
|
648
675
|
"pathPattern": "/sites",
|
|
@@ -696,13 +723,15 @@
|
|
|
696
723
|
"pathPattern": "/sites/{site-id}/lists/{list-id}/items",
|
|
697
724
|
"method": "get",
|
|
698
725
|
"toolName": "list-sharepoint-site-list-items",
|
|
699
|
-
"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."
|
|
700
728
|
},
|
|
701
729
|
{
|
|
702
730
|
"pathPattern": "/sites/{site-id}/lists/{list-id}/items/{listItem-id}",
|
|
703
731
|
"method": "get",
|
|
704
732
|
"toolName": "get-sharepoint-site-list-item",
|
|
705
|
-
"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."
|
|
706
735
|
},
|
|
707
736
|
{
|
|
708
737
|
"pathPattern": "/sites/{site-id}/getByPath(path='{path}')",
|
|
@@ -767,18 +796,21 @@
|
|
|
767
796
|
"pathPattern": "/groups/{group-id}/conversations",
|
|
768
797
|
"method": "get",
|
|
769
798
|
"toolName": "list-group-conversations",
|
|
770
|
-
"workScopes": ["Group.Read.All"]
|
|
799
|
+
"workScopes": ["Group.Read.All"],
|
|
800
|
+
"llmTip": "Legacy — Microsoft recommends Teams channels instead of group conversations."
|
|
771
801
|
},
|
|
772
802
|
{
|
|
773
803
|
"pathPattern": "/groups/{group-id}/threads",
|
|
774
804
|
"method": "get",
|
|
775
805
|
"toolName": "list-group-threads",
|
|
776
|
-
"workScopes": ["Group.Read.All"]
|
|
806
|
+
"workScopes": ["Group.Read.All"],
|
|
807
|
+
"llmTip": "Legacy — Microsoft recommends Teams channels instead of group threads."
|
|
777
808
|
},
|
|
778
809
|
{
|
|
779
810
|
"pathPattern": "/groups/{group-id}/threads/{conversationThread-id}/reply",
|
|
780
811
|
"method": "post",
|
|
781
812
|
"toolName": "reply-to-group-thread",
|
|
782
|
-
"workScopes": ["Group.ReadWrite.All"]
|
|
813
|
+
"workScopes": ["Group.ReadWrite.All"],
|
|
814
|
+
"llmTip": "Legacy — Microsoft recommends Teams channels instead of group threads."
|
|
783
815
|
}
|
|
784
816
|
]
|
package/tsup.config.ts
CHANGED