@juvantlabs/m365-graph-mcp-server 0.1.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.
Files changed (110) hide show
  1. package/ARCHITECTURE.md +225 -0
  2. package/CHANGELOG.md +188 -0
  3. package/LICENSE +21 -0
  4. package/README.md +164 -0
  5. package/SECURITY.md +64 -0
  6. package/dist/auth/confirmation_tokens.d.ts +38 -0
  7. package/dist/auth/confirmation_tokens.d.ts.map +1 -0
  8. package/dist/auth/confirmation_tokens.js +85 -0
  9. package/dist/auth/confirmation_tokens.js.map +1 -0
  10. package/dist/auth/keyring.d.ts +20 -0
  11. package/dist/auth/keyring.d.ts.map +1 -0
  12. package/dist/auth/keyring.js +41 -0
  13. package/dist/auth/keyring.js.map +1 -0
  14. package/dist/auth/msal.d.ts +42 -0
  15. package/dist/auth/msal.d.ts.map +1 -0
  16. package/dist/auth/msal.js +96 -0
  17. package/dist/auth/msal.js.map +1 -0
  18. package/dist/auth/setup.d.ts +18 -0
  19. package/dist/auth/setup.d.ts.map +1 -0
  20. package/dist/auth/setup.js +110 -0
  21. package/dist/auth/setup.js.map +1 -0
  22. package/dist/client/graph.d.ts +30 -0
  23. package/dist/client/graph.d.ts.map +1 -0
  24. package/dist/client/graph.js +38 -0
  25. package/dist/client/graph.js.map +1 -0
  26. package/dist/index.d.ts +54 -0
  27. package/dist/index.d.ts.map +1 -0
  28. package/dist/index.js +131 -0
  29. package/dist/index.js.map +1 -0
  30. package/dist/tools/cancel_event.d.ts +18 -0
  31. package/dist/tools/cancel_event.d.ts.map +1 -0
  32. package/dist/tools/cancel_event.js +95 -0
  33. package/dist/tools/cancel_event.js.map +1 -0
  34. package/dist/tools/copy_file.d.ts +39 -0
  35. package/dist/tools/copy_file.d.ts.map +1 -0
  36. package/dist/tools/copy_file.js +168 -0
  37. package/dist/tools/copy_file.js.map +1 -0
  38. package/dist/tools/create_event.d.ts +29 -0
  39. package/dist/tools/create_event.d.ts.map +1 -0
  40. package/dist/tools/create_event.js +144 -0
  41. package/dist/tools/create_event.js.map +1 -0
  42. package/dist/tools/decline_event.d.ts +18 -0
  43. package/dist/tools/decline_event.d.ts.map +1 -0
  44. package/dist/tools/decline_event.js +105 -0
  45. package/dist/tools/decline_event.js.map +1 -0
  46. package/dist/tools/delete_file.d.ts +28 -0
  47. package/dist/tools/delete_file.d.ts.map +1 -0
  48. package/dist/tools/delete_file.js +103 -0
  49. package/dist/tools/delete_file.js.map +1 -0
  50. package/dist/tools/download_file.d.ts +43 -0
  51. package/dist/tools/download_file.d.ts.map +1 -0
  52. package/dist/tools/download_file.js +133 -0
  53. package/dist/tools/download_file.js.map +1 -0
  54. package/dist/tools/get_event.d.ts +27 -0
  55. package/dist/tools/get_event.d.ts.map +1 -0
  56. package/dist/tools/get_event.js +55 -0
  57. package/dist/tools/get_event.js.map +1 -0
  58. package/dist/tools/index.d.ts +13 -0
  59. package/dist/tools/index.d.ts.map +1 -0
  60. package/dist/tools/index.js +61 -0
  61. package/dist/tools/index.js.map +1 -0
  62. package/dist/tools/list_calendars.d.ts +26 -0
  63. package/dist/tools/list_calendars.d.ts.map +1 -0
  64. package/dist/tools/list_calendars.js +60 -0
  65. package/dist/tools/list_calendars.js.map +1 -0
  66. package/dist/tools/list_drives.d.ts +27 -0
  67. package/dist/tools/list_drives.d.ts.map +1 -0
  68. package/dist/tools/list_drives.js +58 -0
  69. package/dist/tools/list_drives.js.map +1 -0
  70. package/dist/tools/list_events.d.ts +51 -0
  71. package/dist/tools/list_events.d.ts.map +1 -0
  72. package/dist/tools/list_events.js +119 -0
  73. package/dist/tools/list_events.js.map +1 -0
  74. package/dist/tools/list_items.d.ts +31 -0
  75. package/dist/tools/list_items.d.ts.map +1 -0
  76. package/dist/tools/list_items.js +81 -0
  77. package/dist/tools/list_items.js.map +1 -0
  78. package/dist/tools/move_file.d.ts +18 -0
  79. package/dist/tools/move_file.d.ts.map +1 -0
  80. package/dist/tools/move_file.js +60 -0
  81. package/dist/tools/move_file.js.map +1 -0
  82. package/dist/tools/search_events.d.ts +25 -0
  83. package/dist/tools/search_events.d.ts.map +1 -0
  84. package/dist/tools/search_events.js +71 -0
  85. package/dist/tools/search_events.js.map +1 -0
  86. package/dist/tools/search_events_content.d.ts +32 -0
  87. package/dist/tools/search_events_content.d.ts.map +1 -0
  88. package/dist/tools/search_events_content.js +106 -0
  89. package/dist/tools/search_events_content.js.map +1 -0
  90. package/dist/tools/search_files.d.ts +30 -0
  91. package/dist/tools/search_files.d.ts.map +1 -0
  92. package/dist/tools/search_files.js +82 -0
  93. package/dist/tools/search_files.js.map +1 -0
  94. package/dist/tools/update_event.d.ts +25 -0
  95. package/dist/tools/update_event.d.ts.map +1 -0
  96. package/dist/tools/update_event.js +123 -0
  97. package/dist/tools/update_event.js.map +1 -0
  98. package/dist/tools/upload_file.d.ts +38 -0
  99. package/dist/tools/upload_file.d.ts.map +1 -0
  100. package/dist/tools/upload_file.js +152 -0
  101. package/dist/tools/upload_file.js.map +1 -0
  102. package/dist/types/tool.d.ts +32 -0
  103. package/dist/types/tool.d.ts.map +1 -0
  104. package/dist/types/tool.js +10 -0
  105. package/dist/types/tool.js.map +1 -0
  106. package/dist/types/validators.d.ts +44 -0
  107. package/dist/types/validators.d.ts.map +1 -0
  108. package/dist/types/validators.js +78 -0
  109. package/dist/types/validators.js.map +1 -0
  110. package/package.json +72 -0
@@ -0,0 +1,144 @@
1
+ /**
2
+ * Tool: m365-graph:create_event
3
+ *
4
+ * Create a new event on the user's primary calendar (or a specified
5
+ * calendar). Wraps Graph `POST /me/events` (or
6
+ * `POST /me/calendars/{id}/events`).
7
+ *
8
+ * Required Graph scope: `Calendars.ReadWrite` (delegated).
9
+ *
10
+ * Send-invitations behavior: by default Graph DOES send meeting
11
+ * invitations to attendees on event creation. To create an event
12
+ * without sending invites, set `send_invitations: false` (we set
13
+ * `responseStatus.response: "organizer"` and don't add the user as
14
+ * attendee — but Graph still notifies the *other* attendees unless
15
+ * the calendar is configured otherwise).
16
+ *
17
+ * Defaults:
18
+ * - timezone: "UTC" if not specified (Graph requires explicit TZ)
19
+ * - body_content_type: "text"
20
+ * - is_all_day: false
21
+ * - attendee.type: "required" (when type omitted in input)
22
+ *
23
+ * No spec/approval pattern here — creation is non-destructive (worst
24
+ * case: clutter on the calendar; the agent can call cancel_event or
25
+ * the user can delete via Outlook UI).
26
+ */
27
+ import { summarizeEvent } from "./list_events.js";
28
+ import { validateOptionalEnum, validateOptionalString, validateRequiredISODate, validateRequiredString, } from "../types/validators.js";
29
+ const BODY_CONTENT_TYPES = ["text", "html"];
30
+ const ATTENDEE_TYPES = ["required", "optional", "resource"];
31
+ const definition = {
32
+ name: "m365-graph:create_event",
33
+ description: "Create a new event on the user's primary calendar (or a specified calendar). Returns the created event with its server-assigned id and webLink. By default Graph sends meeting invitations to attendees.",
34
+ inputSchema: {
35
+ type: "object",
36
+ properties: {
37
+ subject: { type: "string", description: "Event subject (title)." },
38
+ start: {
39
+ type: "string",
40
+ description: "Start, ISO 8601 (e.g. '2026-05-04T10:00:00').",
41
+ },
42
+ end: {
43
+ type: "string",
44
+ description: "End, ISO 8601. Must be after start.",
45
+ },
46
+ timezone: {
47
+ type: "string",
48
+ description: "IANA timezone for start + end (e.g. 'Europe/Rome'). Default: 'UTC'.",
49
+ },
50
+ body: {
51
+ type: "string",
52
+ description: "Optional body content for the event description.",
53
+ },
54
+ body_content_type: {
55
+ type: "string",
56
+ enum: ["text", "html"],
57
+ description: "Body content type. Default: 'text'.",
58
+ },
59
+ location: {
60
+ type: "string",
61
+ description: "Optional location (free-text or room name).",
62
+ },
63
+ attendees: {
64
+ type: "array",
65
+ description: "Optional attendees. Each item: {email, name?, type?}. type ∈ {required, optional, resource}.",
66
+ items: {
67
+ type: "object",
68
+ properties: {
69
+ email: { type: "string" },
70
+ name: { type: "string" },
71
+ type: { type: "string", enum: ["required", "optional", "resource"] },
72
+ },
73
+ required: ["email"],
74
+ },
75
+ },
76
+ is_all_day: {
77
+ type: "boolean",
78
+ description: "Mark as all-day event. Default: false.",
79
+ },
80
+ calendar_id: {
81
+ type: "string",
82
+ description: "Optional calendar id from list_calendars. Defaults to the user's primary calendar.",
83
+ },
84
+ },
85
+ required: ["subject", "start", "end"],
86
+ },
87
+ };
88
+ function validateAttendees(value) {
89
+ if (value === undefined || value === null)
90
+ return [];
91
+ if (!Array.isArray(value)) {
92
+ throw new Error("'attendees' must be an array");
93
+ }
94
+ return value.map((raw, i) => {
95
+ if (typeof raw !== "object" || raw === null) {
96
+ throw new Error(`'attendees[${i}]' must be an object`);
97
+ }
98
+ const a = raw;
99
+ const email = validateRequiredString(a.email, `attendees[${i}].email`);
100
+ const name = validateOptionalString(a.name, `attendees[${i}].name`);
101
+ const type = validateOptionalEnum(a.type, `attendees[${i}].type`, ATTENDEE_TYPES, "required");
102
+ return { email, name, type };
103
+ });
104
+ }
105
+ const handler = async (graph, args) => {
106
+ const subject = validateRequiredString(args.subject, "subject");
107
+ const start = validateRequiredISODate(args.start, "start");
108
+ const end = validateRequiredISODate(args.end, "end");
109
+ const timezone = validateOptionalString(args.timezone, "timezone") ?? "UTC";
110
+ const body = validateOptionalString(args.body, "body");
111
+ const bodyContentType = validateOptionalEnum(args.body_content_type, "body_content_type", BODY_CONTENT_TYPES, "text");
112
+ const location = validateOptionalString(args.location, "location");
113
+ const attendees = validateAttendees(args.attendees);
114
+ const isAllDay = args.is_all_day === true;
115
+ const calendarId = validateOptionalString(args.calendar_id, "calendar_id");
116
+ const eventBody = {
117
+ subject,
118
+ start: { dateTime: start, timeZone: timezone },
119
+ end: { dateTime: end, timeZone: timezone },
120
+ isAllDay,
121
+ };
122
+ if (body !== undefined) {
123
+ eventBody.body = { contentType: bodyContentType, content: body };
124
+ }
125
+ if (location !== undefined) {
126
+ eventBody.location = { displayName: location };
127
+ }
128
+ if (attendees.length > 0) {
129
+ eventBody.attendees = attendees.map((a) => ({
130
+ emailAddress: a.name ? { address: a.email, name: a.name } : { address: a.email },
131
+ type: a.type ?? "required",
132
+ }));
133
+ }
134
+ const apiPath = calendarId
135
+ ? `/me/calendars/${encodeURIComponent(calendarId)}/events`
136
+ : "/me/events";
137
+ const created = await graph.api(apiPath).post(eventBody);
138
+ const summary = summarizeEvent(created);
139
+ return {
140
+ content: [{ type: "text", text: JSON.stringify({ created: summary }, null, 2) }],
141
+ };
142
+ };
143
+ export const createEventTool = { definition, handler };
144
+ //# sourceMappingURL=create_event.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"create_event.js","sourceRoot":"","sources":["../../src/tools/create_event.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Tool: m365-graph:decline_event
3
+ *
4
+ * Decline an event the user is invited to (as an attendee — not as
5
+ * organizer). Sends a decline notification to the organizer.
6
+ *
7
+ * For events the user *organizes*, use `cancel_event` instead.
8
+ *
9
+ * Two-phase spec/approval pattern (same as delete_file +
10
+ * cancel_event): 1st call returns event preview + confirmation_token,
11
+ * 2nd call (with same args + token) executes POST
12
+ * `/me/events/{id}/decline`.
13
+ *
14
+ * Required Graph scope: `Calendars.ReadWrite` (delegated).
15
+ */
16
+ import type { Tool } from "../types/tool.js";
17
+ export declare const declineEventTool: Tool;
18
+ //# sourceMappingURL=decline_event.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"decline_event.d.ts","sourceRoot":"","sources":["../../src/tools/decline_event.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAaH,OAAO,KAAK,EAAE,IAAI,EAA6C,MAAM,kBAAkB,CAAC;AAiHxF,eAAO,MAAM,gBAAgB,EAAE,IAA8B,CAAC"}
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Tool: m365-graph:decline_event
3
+ *
4
+ * Decline an event the user is invited to (as an attendee — not as
5
+ * organizer). Sends a decline notification to the organizer.
6
+ *
7
+ * For events the user *organizes*, use `cancel_event` instead.
8
+ *
9
+ * Two-phase spec/approval pattern (same as delete_file +
10
+ * cancel_event): 1st call returns event preview + confirmation_token,
11
+ * 2nd call (with same args + token) executes POST
12
+ * `/me/events/{id}/decline`.
13
+ *
14
+ * Required Graph scope: `Calendars.ReadWrite` (delegated).
15
+ */
16
+ import { consumeConfirmation, issueConfirmation, } from "../auth/confirmation_tokens.js";
17
+ import { summarizeEvent } from "./list_events.js";
18
+ import { validateOptionalString, validateRequiredString, } from "../types/validators.js";
19
+ const TOOL_NAME = "m365-graph:decline_event";
20
+ const definition = {
21
+ name: TOOL_NAME,
22
+ description: "Decline an event the user is invited to (as attendee). Sends a decline RSVP to the organizer. Two-phase: first call returns event preview + confirmation_token, second call executes. For events the user organizes, use cancel_event instead.",
23
+ inputSchema: {
24
+ type: "object",
25
+ properties: {
26
+ event_id: { type: "string", description: "Event to decline." },
27
+ comment: {
28
+ type: "string",
29
+ description: "Optional comment included in the decline notification sent to the organizer.",
30
+ },
31
+ send_response: {
32
+ type: "boolean",
33
+ description: "Whether to send a decline notice to the organizer (default true). If false, the decline is silent — organizer does not see a notification.",
34
+ },
35
+ confirmation_token: {
36
+ type: "string",
37
+ description: "Omit on the first call (preview mode). Include the token from the preview to execute the decline.",
38
+ },
39
+ },
40
+ required: ["event_id"],
41
+ },
42
+ };
43
+ const handler = async (graph, args) => {
44
+ const eventId = validateRequiredString(args.event_id, "event_id");
45
+ const comment = validateOptionalString(args.comment, "comment");
46
+ const confirmationToken = validateOptionalString(args.confirmation_token, "confirmation_token");
47
+ // sendResponse default true (Graph default + matches user expectation
48
+ // of "I declined, the organizer should know"). To suppress: pass
49
+ // explicit false.
50
+ const sendResponse = args.send_response === false ? false : true;
51
+ // Spec includes everything that affects what the action does.
52
+ const spec = { event_id: eventId, send_response: sendResponse };
53
+ if (comment !== undefined)
54
+ spec.comment = comment;
55
+ const apiPath = `/me/events/${encodeURIComponent(eventId)}`;
56
+ // ---------- Phase 1: preview ----------
57
+ if (!confirmationToken) {
58
+ const event = (await graph.api(apiPath).get());
59
+ const summary = summarizeEvent(event);
60
+ const issued = issueConfirmation(TOOL_NAME, spec);
61
+ return {
62
+ content: [
63
+ {
64
+ type: "text",
65
+ text: JSON.stringify({
66
+ preview: {
67
+ event: summary,
68
+ decline_comment: comment ?? null,
69
+ send_response: sendResponse,
70
+ ...issued,
71
+ instructions: `Re-call ${TOOL_NAME} with the SAME args (event_id, send_response${comment ? ", comment" : ""}) ` +
72
+ `plus this confirmation_token to send the decline.`,
73
+ },
74
+ }, null, 2),
75
+ },
76
+ ],
77
+ };
78
+ }
79
+ // ---------- Phase 2: execute ----------
80
+ const verdict = consumeConfirmation(confirmationToken, TOOL_NAME, spec);
81
+ if (!verdict.ok) {
82
+ throw new Error(`Refusing to decline: confirmation_token ${verdict.error}. ` +
83
+ `Re-run without confirmation_token to get a fresh preview + token.`);
84
+ }
85
+ const declineBody = { sendResponse };
86
+ if (comment !== undefined)
87
+ declineBody.comment = comment;
88
+ await graph.api(`${apiPath}/decline`).post(declineBody);
89
+ return {
90
+ content: [
91
+ {
92
+ type: "text",
93
+ text: JSON.stringify({
94
+ declined: { event_id: eventId, send_response: sendResponse },
95
+ note: "POST /events/{id}/decline executed. " +
96
+ (sendResponse
97
+ ? "Decline notice sent to the organizer."
98
+ : "Decline was silent — organizer not notified."),
99
+ }, null, 2),
100
+ },
101
+ ],
102
+ };
103
+ };
104
+ export const declineEventTool = { definition, handler };
105
+ //# sourceMappingURL=decline_event.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"decline_event.js","sourceRoot":"","sources":["../../src/tools/decline_event.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Tool: m365-graph:delete_file
3
+ *
4
+ * Delete a file or folder. Two-phase spec/approval pattern per
5
+ * handbook ADR 0002 § Delete-class operations:
6
+ *
7
+ * Call 1 (no confirmation_token):
8
+ * - Tool fetches item metadata for preview.
9
+ * - Issues a confirmation_token tied to the spec
10
+ * {item_id, drive_id} and to this tool name.
11
+ * - Returns the preview + token + expiry hint.
12
+ *
13
+ * Call 2 (with confirmation_token):
14
+ * - Tool verifies the token (exists, not expired, tied to this
15
+ * tool, spec matches).
16
+ * - DELETEs the item.
17
+ * - Consumes the token (single-use).
18
+ *
19
+ * The pattern means a deletion can never happen from a single
20
+ * agent/LLM message — the agent must explicitly review the preview
21
+ * and decide to proceed. Defense against accidental destructive
22
+ * actions in long agent loops.
23
+ *
24
+ * Required Graph scope: `Files.ReadWrite` (delegated).
25
+ */
26
+ import type { Tool } from "../types/tool.js";
27
+ export declare const deleteFileTool: Tool;
28
+ //# sourceMappingURL=delete_file.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"delete_file.d.ts","sourceRoot":"","sources":["../../src/tools/delete_file.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAYH,OAAO,KAAK,EAAE,IAAI,EAA6C,MAAM,kBAAkB,CAAC;AAgHxF,eAAO,MAAM,cAAc,EAAE,IAA8B,CAAC"}
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Tool: m365-graph:delete_file
3
+ *
4
+ * Delete a file or folder. Two-phase spec/approval pattern per
5
+ * handbook ADR 0002 § Delete-class operations:
6
+ *
7
+ * Call 1 (no confirmation_token):
8
+ * - Tool fetches item metadata for preview.
9
+ * - Issues a confirmation_token tied to the spec
10
+ * {item_id, drive_id} and to this tool name.
11
+ * - Returns the preview + token + expiry hint.
12
+ *
13
+ * Call 2 (with confirmation_token):
14
+ * - Tool verifies the token (exists, not expired, tied to this
15
+ * tool, spec matches).
16
+ * - DELETEs the item.
17
+ * - Consumes the token (single-use).
18
+ *
19
+ * The pattern means a deletion can never happen from a single
20
+ * agent/LLM message — the agent must explicitly review the preview
21
+ * and decide to proceed. Defense against accidental destructive
22
+ * actions in long agent loops.
23
+ *
24
+ * Required Graph scope: `Files.ReadWrite` (delegated).
25
+ */
26
+ import { consumeConfirmation, issueConfirmation, } from "../auth/confirmation_tokens.js";
27
+ import { validateOptionalString, validateRequiredString, } from "../types/validators.js";
28
+ const TOOL_NAME = "m365-graph:delete_file";
29
+ const definition = {
30
+ name: TOOL_NAME,
31
+ description: "Delete a file or folder. Two-phase: first call returns a preview + confirmation_token; second call (with the token + same args) executes the delete. Token is single-use, expires in 5 minutes, and tied to the exact spec — passing a different item_id with someone else's token fails.",
32
+ inputSchema: {
33
+ type: "object",
34
+ properties: {
35
+ item_id: { type: "string", description: "Item to delete." },
36
+ drive_id: { type: "string", description: "Optional drive ID. Defaults to /me/drive." },
37
+ confirmation_token: {
38
+ type: "string",
39
+ description: "Omit on the first call (preview mode). Include the token returned by the preview call to execute the delete.",
40
+ },
41
+ },
42
+ required: ["item_id"],
43
+ },
44
+ };
45
+ function summarizePreview(item) {
46
+ const folder = item.folder !== undefined;
47
+ const parent = item.parentReference;
48
+ return {
49
+ id: String(item.id ?? ""),
50
+ name: String(item.name ?? ""),
51
+ type: folder ? "folder" : "file",
52
+ size: Number(item.size ?? 0),
53
+ parent_path: parent?.path ?? null,
54
+ webUrl: String(item.webUrl ?? ""),
55
+ };
56
+ }
57
+ const handler = async (graph, args) => {
58
+ const itemId = validateRequiredString(args.item_id, "item_id");
59
+ const driveId = validateOptionalString(args.drive_id, "drive_id");
60
+ const confirmationToken = validateOptionalString(args.confirmation_token, "confirmation_token");
61
+ const drivePath = driveId
62
+ ? `/drives/${encodeURIComponent(driveId)}`
63
+ : "/me/drive";
64
+ const apiPath = `${drivePath}/items/${encodeURIComponent(itemId)}`;
65
+ const spec = { item_id: itemId };
66
+ if (driveId !== undefined)
67
+ spec.drive_id = driveId;
68
+ // ---------- Phase 1: preview ----------
69
+ if (!confirmationToken) {
70
+ const item = (await graph.api(apiPath).get());
71
+ const summary = summarizePreview(item);
72
+ const issued = issueConfirmation(TOOL_NAME, spec);
73
+ const preview = {
74
+ item: summary,
75
+ ...issued,
76
+ instructions: `Re-call ${TOOL_NAME} with the SAME args (item_id${driveId ? ", drive_id" : ""}) ` +
77
+ `plus this confirmation_token to execute the delete.`,
78
+ };
79
+ return {
80
+ content: [{ type: "text", text: JSON.stringify({ preview }, null, 2) }],
81
+ };
82
+ }
83
+ // ---------- Phase 2: execute ----------
84
+ const verdict = consumeConfirmation(confirmationToken, TOOL_NAME, spec);
85
+ if (!verdict.ok) {
86
+ throw new Error(`Refusing to delete: confirmation_token ${verdict.error}. ` +
87
+ `Re-run without confirmation_token to get a fresh preview + token.`);
88
+ }
89
+ await graph.api(apiPath).delete();
90
+ return {
91
+ content: [
92
+ {
93
+ type: "text",
94
+ text: JSON.stringify({
95
+ deleted: { item_id: itemId, drive_id: driveId ?? null },
96
+ note: "DELETE executed against Graph. The item may be recoverable from the user's recycle bin (default 30-day retention).",
97
+ }, null, 2),
98
+ },
99
+ ],
100
+ };
101
+ };
102
+ export const deleteFileTool = { definition, handler };
103
+ //# sourceMappingURL=delete_file.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"delete_file.js","sourceRoot":"","sources":["../../src/tools/delete_file.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Tool: m365-graph:download_file
3
+ *
4
+ * Download a file from a drive to a per-tenant local sandbox directory,
5
+ * returning the local path. The agent can then read the file via any
6
+ * filesystem-aware MCP (or the agent's built-in file ops) — this tool
7
+ * does not return the file content directly.
8
+ *
9
+ * Why path return, not content:
10
+ * - Files can be large (handbook spec § Performance: 200 MB cap).
11
+ * - MCP responses are JSON-typed; binary content via base64 inflates
12
+ * by 33% and stresses the agent's context.
13
+ * - Composability: M365-aware fetch + provider-agnostic read are
14
+ * two concerns that cleanly separate.
15
+ *
16
+ * Sandboxing (handbook spec § Sandboxing + anti-pattern #1):
17
+ * - All downloads land under <sandbox-root>/<tenant-id>/ where the
18
+ * filename is constructed as <sha256(item_id)[:16]>-<sanitized name>.
19
+ * The local filename is server-controlled, NOT derived from
20
+ * caller-supplied paths. Path injection is structurally impossible.
21
+ * - sandbox-root is M365_DOWNLOAD_DIR (env), or
22
+ * $XDG_CACHE_HOME/m365-graph-mcp-server, or ~/.cache/m365-graph-mcp-server.
23
+ * - 0o700 dir mode + 0o600 file mode (defense-in-depth on shared hosts).
24
+ * - Resolved path is verified to start with the sandbox root before
25
+ * writing — an extra check on top of the structurally-safe naming.
26
+ *
27
+ * Size cap: 200 MB (handbook spec § Performance characteristics). Item
28
+ * size is fetched via `/items/{id}` metadata BEFORE downloading; the
29
+ * tool refuses to stream large files.
30
+ *
31
+ * Streaming: the actual download streams from the Graph CDN
32
+ * (@microsoft.graph.downloadUrl is a pre-signed Akamai URL — no Graph
33
+ * auth needed for the bytes themselves). Streamed via fetch + Node
34
+ * streams pipeline (no whole-file buffering — handbook anti-pattern #11).
35
+ *
36
+ * Required Graph scope: `Files.Read` (delegated). Read-only on the
37
+ * cloud side; writes only to the sandbox locally.
38
+ */
39
+ import type { Tool } from "../types/tool.js";
40
+ export declare function getSandboxRoot(tenantId: string): string;
41
+ export declare function deriveSafeLocalPath(sandboxRoot: string, itemId: string, originalName: string): string;
42
+ export declare const downloadFileTool: Tool;
43
+ //# sourceMappingURL=download_file.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"download_file.d.ts","sourceRoot":"","sources":["../../src/tools/download_file.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AAiBH,OAAO,KAAK,EAAE,IAAI,EAA6C,MAAM,kBAAkB,CAAC;AA2BxF,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAQvD;AAED,wBAAgB,mBAAmB,CAAC,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM,CAWrG;AAgED,eAAO,MAAM,gBAAgB,EAAE,IAA8B,CAAC"}
@@ -0,0 +1,133 @@
1
+ /**
2
+ * Tool: m365-graph:download_file
3
+ *
4
+ * Download a file from a drive to a per-tenant local sandbox directory,
5
+ * returning the local path. The agent can then read the file via any
6
+ * filesystem-aware MCP (or the agent's built-in file ops) — this tool
7
+ * does not return the file content directly.
8
+ *
9
+ * Why path return, not content:
10
+ * - Files can be large (handbook spec § Performance: 200 MB cap).
11
+ * - MCP responses are JSON-typed; binary content via base64 inflates
12
+ * by 33% and stresses the agent's context.
13
+ * - Composability: M365-aware fetch + provider-agnostic read are
14
+ * two concerns that cleanly separate.
15
+ *
16
+ * Sandboxing (handbook spec § Sandboxing + anti-pattern #1):
17
+ * - All downloads land under <sandbox-root>/<tenant-id>/ where the
18
+ * filename is constructed as <sha256(item_id)[:16]>-<sanitized name>.
19
+ * The local filename is server-controlled, NOT derived from
20
+ * caller-supplied paths. Path injection is structurally impossible.
21
+ * - sandbox-root is M365_DOWNLOAD_DIR (env), or
22
+ * $XDG_CACHE_HOME/m365-graph-mcp-server, or ~/.cache/m365-graph-mcp-server.
23
+ * - 0o700 dir mode + 0o600 file mode (defense-in-depth on shared hosts).
24
+ * - Resolved path is verified to start with the sandbox root before
25
+ * writing — an extra check on top of the structurally-safe naming.
26
+ *
27
+ * Size cap: 200 MB (handbook spec § Performance characteristics). Item
28
+ * size is fetched via `/items/{id}` metadata BEFORE downloading; the
29
+ * tool refuses to stream large files.
30
+ *
31
+ * Streaming: the actual download streams from the Graph CDN
32
+ * (@microsoft.graph.downloadUrl is a pre-signed Akamai URL — no Graph
33
+ * auth needed for the bytes themselves). Streamed via fetch + Node
34
+ * streams pipeline (no whole-file buffering — handbook anti-pattern #11).
35
+ *
36
+ * Required Graph scope: `Files.Read` (delegated). Read-only on the
37
+ * cloud side; writes only to the sandbox locally.
38
+ */
39
+ import { createWriteStream } from "node:fs";
40
+ import { mkdir } from "node:fs/promises";
41
+ import os from "node:os";
42
+ import path from "node:path";
43
+ import crypto from "node:crypto";
44
+ import { Readable } from "node:stream";
45
+ import { pipeline } from "node:stream/promises";
46
+ import { sanitizeFilename, validateOptionalString, validateRequiredString, } from "../types/validators.js";
47
+ const MAX_FILE_SIZE_BYTES = 200 * 1024 * 1024; // 200 MB per handbook spec
48
+ const SANDBOX_DIR_MODE = 0o700;
49
+ const SANDBOX_FILE_MODE = 0o600;
50
+ const definition = {
51
+ name: "m365-graph:download_file",
52
+ description: "Download a file from a drive to a local sandbox directory. Returns the local path, not the file content — agents read the file via a filesystem-aware tool. Size is capped at 200 MB. Read-only on the cloud side.",
53
+ inputSchema: {
54
+ type: "object",
55
+ properties: {
56
+ item_id: {
57
+ type: "string",
58
+ description: "ID of the file to download. Obtain from list_items or search_files. Folders are rejected (no content).",
59
+ },
60
+ drive_id: {
61
+ type: "string",
62
+ description: "Optional drive ID. Defaults to the user's primary OneDrive.",
63
+ },
64
+ },
65
+ required: ["item_id"],
66
+ },
67
+ };
68
+ export function getSandboxRoot(tenantId) {
69
+ const override = process.env.M365_DOWNLOAD_DIR;
70
+ if (override)
71
+ return path.resolve(override, tenantId);
72
+ const xdg = process.env.XDG_CACHE_HOME;
73
+ if (xdg)
74
+ return path.resolve(xdg, "m365-graph-mcp-server", tenantId);
75
+ return path.resolve(os.homedir(), ".cache", "m365-graph-mcp-server", tenantId);
76
+ }
77
+ export function deriveSafeLocalPath(sandboxRoot, itemId, originalName) {
78
+ const itemHash = crypto.createHash("sha256").update(itemId).digest("hex").slice(0, 16);
79
+ const safe = sanitizeFilename(originalName);
80
+ const filename = `${itemHash}-${safe}`;
81
+ const resolved = path.resolve(sandboxRoot, filename);
82
+ // Defense-in-depth even though the filename is server-constructed.
83
+ if (!resolved.startsWith(sandboxRoot + path.sep) && resolved !== sandboxRoot) {
84
+ throw new Error(`Refusing to write outside sandbox: ${resolved}`);
85
+ }
86
+ return resolved;
87
+ }
88
+ const handler = async (graph, args) => {
89
+ const itemId = validateRequiredString(args.item_id, "item_id");
90
+ const driveId = validateOptionalString(args.drive_id, "drive_id");
91
+ const driveRoot = driveId ? `/drives/${encodeURIComponent(driveId)}` : "/me/drive";
92
+ const item = await graph.api(`${driveRoot}/items/${encodeURIComponent(itemId)}`).get();
93
+ if (item.folder !== undefined) {
94
+ throw new Error(`Item ${itemId} is a folder, not a file — refusing to download.`);
95
+ }
96
+ const size = Number(item.size ?? 0);
97
+ if (size > MAX_FILE_SIZE_BYTES) {
98
+ throw new Error(`File size (${size} bytes) exceeds the 200 MB cap. Refusing to download.`);
99
+ }
100
+ const downloadUrl = item["@microsoft.graph.downloadUrl"];
101
+ if (!downloadUrl) {
102
+ throw new Error("No @microsoft.graph.downloadUrl in item metadata — Graph returned an unexpected shape.");
103
+ }
104
+ const tenantId = process.env.M365_TENANT_ID ?? "unknown";
105
+ const sandboxRoot = getSandboxRoot(tenantId);
106
+ await mkdir(sandboxRoot, { recursive: true, mode: SANDBOX_DIR_MODE });
107
+ const localPath = deriveSafeLocalPath(sandboxRoot, itemId, String(item.name ?? "file"));
108
+ const response = await fetch(downloadUrl);
109
+ if (!response.ok) {
110
+ throw new Error(`Download HTTP ${response.status}: ${response.statusText}`);
111
+ }
112
+ if (!response.body) {
113
+ throw new Error("Download response has no body stream.");
114
+ }
115
+ const out = createWriteStream(localPath, { mode: SANDBOX_FILE_MODE });
116
+ // Node 20+ Web→Node stream interop. The `as ReadableStream` cast is
117
+ // because node:stream's Readable.fromWeb expects the Node-bundled
118
+ // ReadableStream, but undici's fetch returns the same shape.
119
+ await pipeline(Readable.fromWeb(response.body), out);
120
+ const result = {
121
+ item_id: itemId,
122
+ drive_id: driveId ?? null,
123
+ local_path: localPath,
124
+ size_bytes: size,
125
+ name: String(item.name ?? ""),
126
+ content_type: item.file?.mimeType ?? null,
127
+ };
128
+ return {
129
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
130
+ };
131
+ };
132
+ export const downloadFileTool = { definition, handler };
133
+ //# sourceMappingURL=download_file.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"download_file.js","sourceRoot":"","sources":["../../src/tools/download_file.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Tool: m365-graph:get_event
3
+ *
4
+ * Fetch a single event's full detail (including the full body,
5
+ * attendees with response statuses, location, recurrence rule for
6
+ * series masters). Wraps Graph `/me/events/{id}`.
7
+ *
8
+ * Required Graph scope: `Calendars.Read` (delegated). Read-only.
9
+ *
10
+ * Input:
11
+ * event_id (string, required) — id from list_events / search_events
12
+ *
13
+ * Output: full event summary plus `body` (the long-form HTML/text
14
+ * preview, capped) and `recurrence` (the recurrence rule if any).
15
+ */
16
+ import { summarizeEvent } from "./list_events.js";
17
+ import type { Tool } from "../types/tool.js";
18
+ interface FullEvent extends ReturnType<typeof summarizeEvent> {
19
+ body_content_type: string;
20
+ body: string;
21
+ body_truncated: boolean;
22
+ recurrence: unknown;
23
+ }
24
+ export declare function expandEvent(event: Record<string, unknown>): FullEvent;
25
+ export declare const getEventTool: Tool;
26
+ export {};
27
+ //# sourceMappingURL=get_event.d.ts.map