@softeria/ms-365-mcp-server 0.46.1 → 0.47.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/__tests__/graph-tools.test.js +355 -0
- package/dist/endpoints.json +81 -31
- package/dist/generated/client.js +190 -0
- package/dist/graph-tools.js +47 -3
- package/logs/mcp-server.log +10 -10
- package/package.json +1 -1
- package/src/endpoints.json +81 -31
- 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,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}')",
|
|
@@ -766,22 +792,46 @@
|
|
|
766
792
|
"returnDownloadUrl": true,
|
|
767
793
|
"llmTip": "Returns a temporary download URL for the meeting recording in MP4 format. Use the download URL to access the video file. The recording may be large — do not attempt to stream it inline."
|
|
768
794
|
},
|
|
795
|
+
{
|
|
796
|
+
"pathPattern": "/me/onlineMeetings/{onlineMeeting-id}/attendanceReports",
|
|
797
|
+
"method": "get",
|
|
798
|
+
"toolName": "list-meeting-attendance-reports",
|
|
799
|
+
"workScopes": ["OnlineMeetingArtifact.Read.All"],
|
|
800
|
+
"llmTip": "Lists attendance reports for a meeting. Each report has meetingStartDateTime, meetingEndDateTime, totalParticipantCount. For recurring meetings, there is one report per occurrence. Get the meeting ID first via list-online-meetings."
|
|
801
|
+
},
|
|
802
|
+
{
|
|
803
|
+
"pathPattern": "/me/onlineMeetings/{onlineMeeting-id}/attendanceReports/{meetingAttendanceReport-id}",
|
|
804
|
+
"method": "get",
|
|
805
|
+
"toolName": "get-meeting-attendance-report",
|
|
806
|
+
"workScopes": ["OnlineMeetingArtifact.Read.All"],
|
|
807
|
+
"llmTip": "Gets a specific attendance report with totalParticipantCount, meetingStartDateTime, meetingEndDateTime."
|
|
808
|
+
},
|
|
809
|
+
{
|
|
810
|
+
"pathPattern": "/me/onlineMeetings/{onlineMeeting-id}/attendanceReports/{meetingAttendanceReport-id}/attendanceRecords",
|
|
811
|
+
"method": "get",
|
|
812
|
+
"toolName": "list-meeting-attendance-records",
|
|
813
|
+
"workScopes": ["OnlineMeetingArtifact.Read.All"],
|
|
814
|
+
"llmTip": "Lists individual attendance records for a meeting report. Each record has: identity (displayName), emailAddress, role (Organizer/Presenter/Attendee), totalAttendanceInSeconds, and attendanceIntervals with joinDateTime/leaveDateTime. Use to determine who attended, how long, and when they joined/left."
|
|
815
|
+
},
|
|
769
816
|
{
|
|
770
817
|
"pathPattern": "/groups/{group-id}/conversations",
|
|
771
818
|
"method": "get",
|
|
772
819
|
"toolName": "list-group-conversations",
|
|
773
|
-
"workScopes": ["Group.Read.All"]
|
|
820
|
+
"workScopes": ["Group.Read.All"],
|
|
821
|
+
"llmTip": "Legacy — Microsoft recommends Teams channels instead of group conversations."
|
|
774
822
|
},
|
|
775
823
|
{
|
|
776
824
|
"pathPattern": "/groups/{group-id}/threads",
|
|
777
825
|
"method": "get",
|
|
778
826
|
"toolName": "list-group-threads",
|
|
779
|
-
"workScopes": ["Group.Read.All"]
|
|
827
|
+
"workScopes": ["Group.Read.All"],
|
|
828
|
+
"llmTip": "Legacy — Microsoft recommends Teams channels instead of group threads."
|
|
780
829
|
},
|
|
781
830
|
{
|
|
782
831
|
"pathPattern": "/groups/{group-id}/threads/{conversationThread-id}/reply",
|
|
783
832
|
"method": "post",
|
|
784
833
|
"toolName": "reply-to-group-thread",
|
|
785
|
-
"workScopes": ["Group.ReadWrite.All"]
|
|
834
|
+
"workScopes": ["Group.ReadWrite.All"],
|
|
835
|
+
"llmTip": "Legacy — Microsoft recommends Teams channels instead of group threads."
|
|
786
836
|
}
|
|
787
837
|
]
|
package/dist/generated/client.js
CHANGED
|
@@ -2873,6 +2873,69 @@ const microsoft_graph_onlineMeetingCollectionResponse = z.object({
|
|
|
2873
2873
|
"@odata.nextLink": z.string().nullable(),
|
|
2874
2874
|
value: z.array(microsoft_graph_onlineMeeting)
|
|
2875
2875
|
}).partial().passthrough();
|
|
2876
|
+
const microsoft_graph_virtualEventExternalInformation = z.object({
|
|
2877
|
+
applicationId: z.string().describe("Identifier of the application that hosts the externalEventId. Read-only.").nullish(),
|
|
2878
|
+
externalEventId: z.string().describe(
|
|
2879
|
+
"The identifier for a virtualEventExternalInformation object that associates the virtual event with an event ID in an external application. This association bundles all the information (both supported and not supported in virtualEvent) into one virtual event object. Optional. If set, the maximum supported length is 256 characters."
|
|
2880
|
+
).nullish()
|
|
2881
|
+
}).passthrough();
|
|
2882
|
+
const microsoft_graph_attendanceInterval = z.object({
|
|
2883
|
+
durationInSeconds: z.number().gte(-2147483648).lte(2147483647).describe(
|
|
2884
|
+
"Duration of the meeting interval in seconds; that is, the difference between joinDateTime and leaveDateTime."
|
|
2885
|
+
).nullish(),
|
|
2886
|
+
joinDateTime: z.string().regex(
|
|
2887
|
+
/^[0-9]{4,}-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])T([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9]([.][0-9]{1,12})?(Z|[+-][0-9][0-9]:[0-9][0-9])$/
|
|
2888
|
+
).datetime({ offset: true }).describe("The time the attendee joined in UTC.").nullish(),
|
|
2889
|
+
leaveDateTime: z.string().regex(
|
|
2890
|
+
/^[0-9]{4,}-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])T([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9]([.][0-9]{1,12})?(Z|[+-][0-9][0-9]:[0-9][0-9])$/
|
|
2891
|
+
).datetime({ offset: true }).describe("The time the attendee left in UTC.").nullish()
|
|
2892
|
+
}).passthrough();
|
|
2893
|
+
const microsoft_graph_virtualEventExternalRegistrationInformation = z.object({
|
|
2894
|
+
referrer: z.string().describe(
|
|
2895
|
+
"A URL or string that represents the location from which the registrant registered. Optional."
|
|
2896
|
+
).nullish(),
|
|
2897
|
+
registrationId: z.string().describe(
|
|
2898
|
+
"The identifier for a virtualEventExternalRegistrationInformation object. Optional. If set, the maximum supported length is 256 characters."
|
|
2899
|
+
).nullish()
|
|
2900
|
+
}).passthrough();
|
|
2901
|
+
const microsoft_graph_attendanceRecord = z.object({
|
|
2902
|
+
id: z.string().describe("The unique identifier for an entity. Read-only.").optional(),
|
|
2903
|
+
attendanceIntervals: z.array(microsoft_graph_attendanceInterval).describe("List of time periods between joining and leaving a meeting.").optional(),
|
|
2904
|
+
emailAddress: z.string().describe("Email address of the user associated with this attendance record.").nullish(),
|
|
2905
|
+
externalRegistrationInformation: microsoft_graph_virtualEventExternalRegistrationInformation.optional(),
|
|
2906
|
+
identity: microsoft_graph_identity.optional(),
|
|
2907
|
+
registrationId: z.string().describe(
|
|
2908
|
+
"Unique identifier of a virtualEventRegistration that is available to all participants registered for the virtualEventWebinar."
|
|
2909
|
+
).nullish(),
|
|
2910
|
+
role: z.string().describe(
|
|
2911
|
+
"Role of the attendee. The possible values are: None, Attendee, Presenter, and Organizer."
|
|
2912
|
+
).nullish(),
|
|
2913
|
+
totalAttendanceInSeconds: z.number().gte(-2147483648).lte(2147483647).describe("Total duration of the attendances in seconds.").nullish()
|
|
2914
|
+
}).passthrough();
|
|
2915
|
+
const microsoft_graph_meetingAttendanceReport = z.object({
|
|
2916
|
+
id: z.string().describe("The unique identifier for an entity. Read-only.").optional(),
|
|
2917
|
+
externalEventInformation: z.array(microsoft_graph_virtualEventExternalInformation).describe(
|
|
2918
|
+
"The external information of a virtual event. Returned only for event organizers or coorganizers. Read-only."
|
|
2919
|
+
).optional(),
|
|
2920
|
+
meetingEndDateTime: z.string().regex(
|
|
2921
|
+
/^[0-9]{4,}-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])T([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9]([.][0-9]{1,12})?(Z|[+-][0-9][0-9]:[0-9][0-9])$/
|
|
2922
|
+
).datetime({ offset: true }).describe("UTC time when the meeting ended. Read-only.").nullish(),
|
|
2923
|
+
meetingStartDateTime: z.string().regex(
|
|
2924
|
+
/^[0-9]{4,}-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])T([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9]([.][0-9]{1,12})?(Z|[+-][0-9][0-9]:[0-9][0-9])$/
|
|
2925
|
+
).datetime({ offset: true }).describe("UTC time when the meeting started. Read-only.").nullish(),
|
|
2926
|
+
totalParticipantCount: z.number().gte(-2147483648).lte(2147483647).describe("Total number of participants. Read-only.").nullish(),
|
|
2927
|
+
attendanceRecords: z.array(microsoft_graph_attendanceRecord).describe("List of attendance records of an attendance report. Read-only.").optional()
|
|
2928
|
+
}).passthrough();
|
|
2929
|
+
const microsoft_graph_meetingAttendanceReportCollectionResponse = z.object({
|
|
2930
|
+
"@odata.count": z.number().int().nullable(),
|
|
2931
|
+
"@odata.nextLink": z.string().nullable(),
|
|
2932
|
+
value: z.array(microsoft_graph_meetingAttendanceReport)
|
|
2933
|
+
}).partial().passthrough();
|
|
2934
|
+
const microsoft_graph_attendanceRecordCollectionResponse = z.object({
|
|
2935
|
+
"@odata.count": z.number().int().nullable(),
|
|
2936
|
+
"@odata.nextLink": z.string().nullable(),
|
|
2937
|
+
value: z.array(microsoft_graph_attendanceRecord)
|
|
2938
|
+
}).partial().passthrough();
|
|
2876
2939
|
const microsoft_graph_callRecording = z.object({
|
|
2877
2940
|
id: z.string().describe("The unique identifier for an entity. Read-only.").optional(),
|
|
2878
2941
|
callId: z.string().describe("The unique identifier for the call that is related to this recording. Read-only.").nullish(),
|
|
@@ -3778,6 +3841,13 @@ const schemas = {
|
|
|
3778
3841
|
microsoft_graph_joinMeetingIdSettings,
|
|
3779
3842
|
microsoft_graph_onlineMeeting,
|
|
3780
3843
|
microsoft_graph_onlineMeetingCollectionResponse,
|
|
3844
|
+
microsoft_graph_virtualEventExternalInformation,
|
|
3845
|
+
microsoft_graph_attendanceInterval,
|
|
3846
|
+
microsoft_graph_virtualEventExternalRegistrationInformation,
|
|
3847
|
+
microsoft_graph_attendanceRecord,
|
|
3848
|
+
microsoft_graph_meetingAttendanceReport,
|
|
3849
|
+
microsoft_graph_meetingAttendanceReportCollectionResponse,
|
|
3850
|
+
microsoft_graph_attendanceRecordCollectionResponse,
|
|
3781
3851
|
microsoft_graph_callRecording,
|
|
3782
3852
|
microsoft_graph_callRecordingCollectionResponse,
|
|
3783
3853
|
microsoft_graph_callTranscript,
|
|
@@ -6377,6 +6447,126 @@ resource.`,
|
|
|
6377
6447
|
],
|
|
6378
6448
|
response: z.void()
|
|
6379
6449
|
},
|
|
6450
|
+
{
|
|
6451
|
+
method: "get",
|
|
6452
|
+
path: "/me/onlineMeetings/:onlineMeetingId/attendanceReports",
|
|
6453
|
+
alias: "list-meeting-attendance-reports",
|
|
6454
|
+
description: `Get a list of meetingAttendanceReport objects for an onlineMeeting or a virtualEvent. Each time an online meeting or a virtual event ends, an attendance report is generated for that session.`,
|
|
6455
|
+
requestFormat: "json",
|
|
6456
|
+
parameters: [
|
|
6457
|
+
{
|
|
6458
|
+
name: "$top",
|
|
6459
|
+
type: "Query",
|
|
6460
|
+
schema: z.number().int().gte(0).describe("Show only the first n items").optional()
|
|
6461
|
+
},
|
|
6462
|
+
{
|
|
6463
|
+
name: "$skip",
|
|
6464
|
+
type: "Query",
|
|
6465
|
+
schema: z.number().int().gte(0).describe("Skip the first n items").optional()
|
|
6466
|
+
},
|
|
6467
|
+
{
|
|
6468
|
+
name: "$search",
|
|
6469
|
+
type: "Query",
|
|
6470
|
+
schema: z.string().describe("Search items by search phrases").optional()
|
|
6471
|
+
},
|
|
6472
|
+
{
|
|
6473
|
+
name: "$filter",
|
|
6474
|
+
type: "Query",
|
|
6475
|
+
schema: z.string().describe("Filter items by property values").optional()
|
|
6476
|
+
},
|
|
6477
|
+
{
|
|
6478
|
+
name: "$count",
|
|
6479
|
+
type: "Query",
|
|
6480
|
+
schema: z.boolean().describe("Include count of items").optional()
|
|
6481
|
+
},
|
|
6482
|
+
{
|
|
6483
|
+
name: "$orderby",
|
|
6484
|
+
type: "Query",
|
|
6485
|
+
schema: z.array(z.string()).describe("Order items by property values").optional()
|
|
6486
|
+
},
|
|
6487
|
+
{
|
|
6488
|
+
name: "$select",
|
|
6489
|
+
type: "Query",
|
|
6490
|
+
schema: z.array(z.string()).describe("Select properties to be returned").optional()
|
|
6491
|
+
},
|
|
6492
|
+
{
|
|
6493
|
+
name: "$expand",
|
|
6494
|
+
type: "Query",
|
|
6495
|
+
schema: z.array(z.string()).describe("Expand related entities").optional()
|
|
6496
|
+
}
|
|
6497
|
+
],
|
|
6498
|
+
response: z.void()
|
|
6499
|
+
},
|
|
6500
|
+
{
|
|
6501
|
+
method: "get",
|
|
6502
|
+
path: "/me/onlineMeetings/:onlineMeetingId/attendanceReports/:meetingAttendanceReportId",
|
|
6503
|
+
alias: "get-meeting-attendance-report",
|
|
6504
|
+
description: `Get the meetingAttendanceReport for an onlineMeeting or a virtualEvent. When an online meeting ends, an attendance report is generated for that session.`,
|
|
6505
|
+
requestFormat: "json",
|
|
6506
|
+
parameters: [
|
|
6507
|
+
{
|
|
6508
|
+
name: "$select",
|
|
6509
|
+
type: "Query",
|
|
6510
|
+
schema: z.array(z.string()).describe("Select properties to be returned").optional()
|
|
6511
|
+
},
|
|
6512
|
+
{
|
|
6513
|
+
name: "$expand",
|
|
6514
|
+
type: "Query",
|
|
6515
|
+
schema: z.array(z.string()).describe("Expand related entities").optional()
|
|
6516
|
+
}
|
|
6517
|
+
],
|
|
6518
|
+
response: z.void()
|
|
6519
|
+
},
|
|
6520
|
+
{
|
|
6521
|
+
method: "get",
|
|
6522
|
+
path: "/me/onlineMeetings/:onlineMeetingId/attendanceReports/:meetingAttendanceReportId/attendanceRecords",
|
|
6523
|
+
alias: "list-meeting-attendance-records",
|
|
6524
|
+
description: `Get a list of attendanceRecord objects and their properties.`,
|
|
6525
|
+
requestFormat: "json",
|
|
6526
|
+
parameters: [
|
|
6527
|
+
{
|
|
6528
|
+
name: "$top",
|
|
6529
|
+
type: "Query",
|
|
6530
|
+
schema: z.number().int().gte(0).describe("Show only the first n items").optional()
|
|
6531
|
+
},
|
|
6532
|
+
{
|
|
6533
|
+
name: "$skip",
|
|
6534
|
+
type: "Query",
|
|
6535
|
+
schema: z.number().int().gte(0).describe("Skip the first n items").optional()
|
|
6536
|
+
},
|
|
6537
|
+
{
|
|
6538
|
+
name: "$search",
|
|
6539
|
+
type: "Query",
|
|
6540
|
+
schema: z.string().describe("Search items by search phrases").optional()
|
|
6541
|
+
},
|
|
6542
|
+
{
|
|
6543
|
+
name: "$filter",
|
|
6544
|
+
type: "Query",
|
|
6545
|
+
schema: z.string().describe("Filter items by property values").optional()
|
|
6546
|
+
},
|
|
6547
|
+
{
|
|
6548
|
+
name: "$count",
|
|
6549
|
+
type: "Query",
|
|
6550
|
+
schema: z.boolean().describe("Include count of items").optional()
|
|
6551
|
+
},
|
|
6552
|
+
{
|
|
6553
|
+
name: "$orderby",
|
|
6554
|
+
type: "Query",
|
|
6555
|
+
schema: z.array(z.string()).describe("Order items by property values").optional()
|
|
6556
|
+
},
|
|
6557
|
+
{
|
|
6558
|
+
name: "$select",
|
|
6559
|
+
type: "Query",
|
|
6560
|
+
schema: z.array(z.string()).describe("Select properties to be returned").optional()
|
|
6561
|
+
},
|
|
6562
|
+
{
|
|
6563
|
+
name: "$expand",
|
|
6564
|
+
type: "Query",
|
|
6565
|
+
schema: z.array(z.string()).describe("Expand related entities").optional()
|
|
6566
|
+
}
|
|
6567
|
+
],
|
|
6568
|
+
response: z.void()
|
|
6569
|
+
},
|
|
6380
6570
|
{
|
|
6381
6571
|
method: "get",
|
|
6382
6572
|
path: "/me/onlineMeetings/:onlineMeetingId/recordings",
|
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 12:50:25 INFO: [GRAPH CLIENT] Final URL being sent to Microsoft: https://graph.microsoft.com/v1.0/me
|
|
2
|
+
2026-03-25 12:50:25 INFO: [GRAPH CLIENT] Final URL being sent to Microsoft: https://graph.microsoft.com/v1.0/me
|
|
3
|
+
2026-03-25 12:50:25 INFO: [GRAPH CLIENT] Final URL being sent to Microsoft: https://graph.microsoft.com/v1.0/me
|
|
4
|
+
2026-03-25 12:50:25 INFO: [GRAPH CLIENT] Final URL being sent to Microsoft: https://graph.microsoft.com/v1.0/me/messages
|
|
5
|
+
2026-03-25 12:50:25 INFO: [GRAPH CLIENT] Final URL being sent to Microsoft: https://graph.microsoft.com/v1.0/me/calendar
|
|
6
|
+
2026-03-25 12:50:26 INFO: Using environment variables for secrets
|
|
7
|
+
2026-03-25 12:50:26 INFO: Using environment variables for secrets
|
|
8
|
+
2026-03-25 12:50:26 INFO: Using environment variables for secrets
|
|
9
|
+
2026-03-25 12:50:26 INFO: Using environment variables for secrets
|
|
10
|
+
2026-03-25 12:50:26 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.
|
|
3
|
+
"version": "0.47.0",
|
|
4
4
|
"description": " A Model Context Protocol (MCP) server for interacting with Microsoft 365 and Office services through the Graph API",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
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,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}')",
|
|
@@ -766,22 +792,46 @@
|
|
|
766
792
|
"returnDownloadUrl": true,
|
|
767
793
|
"llmTip": "Returns a temporary download URL for the meeting recording in MP4 format. Use the download URL to access the video file. The recording may be large — do not attempt to stream it inline."
|
|
768
794
|
},
|
|
795
|
+
{
|
|
796
|
+
"pathPattern": "/me/onlineMeetings/{onlineMeeting-id}/attendanceReports",
|
|
797
|
+
"method": "get",
|
|
798
|
+
"toolName": "list-meeting-attendance-reports",
|
|
799
|
+
"workScopes": ["OnlineMeetingArtifact.Read.All"],
|
|
800
|
+
"llmTip": "Lists attendance reports for a meeting. Each report has meetingStartDateTime, meetingEndDateTime, totalParticipantCount. For recurring meetings, there is one report per occurrence. Get the meeting ID first via list-online-meetings."
|
|
801
|
+
},
|
|
802
|
+
{
|
|
803
|
+
"pathPattern": "/me/onlineMeetings/{onlineMeeting-id}/attendanceReports/{meetingAttendanceReport-id}",
|
|
804
|
+
"method": "get",
|
|
805
|
+
"toolName": "get-meeting-attendance-report",
|
|
806
|
+
"workScopes": ["OnlineMeetingArtifact.Read.All"],
|
|
807
|
+
"llmTip": "Gets a specific attendance report with totalParticipantCount, meetingStartDateTime, meetingEndDateTime."
|
|
808
|
+
},
|
|
809
|
+
{
|
|
810
|
+
"pathPattern": "/me/onlineMeetings/{onlineMeeting-id}/attendanceReports/{meetingAttendanceReport-id}/attendanceRecords",
|
|
811
|
+
"method": "get",
|
|
812
|
+
"toolName": "list-meeting-attendance-records",
|
|
813
|
+
"workScopes": ["OnlineMeetingArtifact.Read.All"],
|
|
814
|
+
"llmTip": "Lists individual attendance records for a meeting report. Each record has: identity (displayName), emailAddress, role (Organizer/Presenter/Attendee), totalAttendanceInSeconds, and attendanceIntervals with joinDateTime/leaveDateTime. Use to determine who attended, how long, and when they joined/left."
|
|
815
|
+
},
|
|
769
816
|
{
|
|
770
817
|
"pathPattern": "/groups/{group-id}/conversations",
|
|
771
818
|
"method": "get",
|
|
772
819
|
"toolName": "list-group-conversations",
|
|
773
|
-
"workScopes": ["Group.Read.All"]
|
|
820
|
+
"workScopes": ["Group.Read.All"],
|
|
821
|
+
"llmTip": "Legacy — Microsoft recommends Teams channels instead of group conversations."
|
|
774
822
|
},
|
|
775
823
|
{
|
|
776
824
|
"pathPattern": "/groups/{group-id}/threads",
|
|
777
825
|
"method": "get",
|
|
778
826
|
"toolName": "list-group-threads",
|
|
779
|
-
"workScopes": ["Group.Read.All"]
|
|
827
|
+
"workScopes": ["Group.Read.All"],
|
|
828
|
+
"llmTip": "Legacy — Microsoft recommends Teams channels instead of group threads."
|
|
780
829
|
},
|
|
781
830
|
{
|
|
782
831
|
"pathPattern": "/groups/{group-id}/threads/{conversationThread-id}/reply",
|
|
783
832
|
"method": "post",
|
|
784
833
|
"toolName": "reply-to-group-thread",
|
|
785
|
-
"workScopes": ["Group.ReadWrite.All"]
|
|
834
|
+
"workScopes": ["Group.ReadWrite.All"],
|
|
835
|
+
"llmTip": "Legacy — Microsoft recommends Teams channels instead of group threads."
|
|
786
836
|
}
|
|
787
837
|
]
|
package/tsup.config.ts
CHANGED