@softeria/ms-365-mcp-server 0.159.1 → 0.161.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/README.md +22 -0
- package/dist/attachment-route.js +140 -1
- package/dist/endpoints.json +74 -0
- package/dist/generated/client.js +273 -18
- package/dist/graph-tools.js +104 -0
- package/dist/lib/attachment-tickets.js +17 -2
- package/dist/server.js +8 -9
- package/dist/tool-categories.js +1 -0
- package/package.json +1 -1
- package/src/endpoints.json +74 -0
package/README.md
CHANGED
|
@@ -776,6 +776,28 @@ credential.
|
|
|
776
776
|
be minted is one `download-bytes` would fetch for the same caller on the same account. The
|
|
777
777
|
ticket only moves those bytes out of the context window and into a direct transfer.
|
|
778
778
|
|
|
779
|
+
### Uploading attachments the same way
|
|
780
|
+
|
|
781
|
+
The same ticket store works in the other direction. `get-upload-url` mints a URL for
|
|
782
|
+
attaching a file to a draft message or an event, so the bytes go straight from the caller
|
|
783
|
+
to Graph instead of passing as base64 `contentBytes` through the agent context:
|
|
784
|
+
|
|
785
|
+
```
|
|
786
|
+
PUT /attachment?t=<ticket>&dgk=<key-id>&dgx=<expiry>&dgs=<signature>
|
|
787
|
+
Content-Length: <file size>
|
|
788
|
+
|
|
789
|
+
<raw file bytes>
|
|
790
|
+
```
|
|
791
|
+
|
|
792
|
+
`target` is the item's attachments collection (`/me/messages/{id}/attachments` for a draft,
|
|
793
|
+
`/me/events/{id}/attachments` for an event); `name` and `contentType` are recorded on the
|
|
794
|
+
attachment. Files under 3 MB are attached inline; larger ones, up to Graph's 150 MB, stream
|
|
795
|
+
through an upload session in 3.75 MiB chunks, so the server never holds more than one chunk.
|
|
796
|
+
`Content-Length` is required. The ticket is single-use and burnt on first presentation
|
|
797
|
+
whatever the outcome, and the upload is attached as the identity that minted it, exactly as
|
|
798
|
+
downloads are fetched. An upload ticket presented to `GET` (or a download ticket to `PUT`)
|
|
799
|
+
is refused with the same 404 as any other bad ticket.
|
|
800
|
+
|
|
779
801
|
### Configuration
|
|
780
802
|
|
|
781
803
|
```
|
package/dist/attachment-route.js
CHANGED
|
@@ -61,7 +61,7 @@ function createAttachmentHandler(deps) {
|
|
|
61
61
|
return;
|
|
62
62
|
}
|
|
63
63
|
const ticket = deps.store.redeem(raw);
|
|
64
|
-
if (!ticket) {
|
|
64
|
+
if (!ticket || ticket.purpose !== "download") {
|
|
65
65
|
refuse(res);
|
|
66
66
|
return;
|
|
67
67
|
}
|
|
@@ -104,7 +104,146 @@ function createAttachmentHandler(deps) {
|
|
|
104
104
|
}
|
|
105
105
|
};
|
|
106
106
|
}
|
|
107
|
+
const MAX_UPLOAD_BYTES = 150 * 1024 * 1024;
|
|
108
|
+
const INLINE_UPLOAD_LIMIT = 3 * 1024 * 1024;
|
|
109
|
+
const UPLOAD_CHUNK_BYTES = 320 * 1024 * 12;
|
|
110
|
+
async function putChunk(uploadUrl, chunk, start, total) {
|
|
111
|
+
const end = start + chunk.length - 1;
|
|
112
|
+
const response = await fetch(uploadUrl, {
|
|
113
|
+
method: "PUT",
|
|
114
|
+
headers: {
|
|
115
|
+
"Content-Type": "application/octet-stream",
|
|
116
|
+
"Content-Length": String(chunk.length),
|
|
117
|
+
"Content-Range": `bytes ${start}-${end}/${total}`
|
|
118
|
+
},
|
|
119
|
+
// A standalone ArrayBuffer: fetch's body typing wants a BufferSource, and a
|
|
120
|
+
// Buffer view over a pooled slab is neither the right type nor the right bytes.
|
|
121
|
+
body: chunk.buffer.slice(chunk.byteOffset, chunk.byteOffset + chunk.byteLength)
|
|
122
|
+
});
|
|
123
|
+
if (![200, 201, 202].includes(response.status)) {
|
|
124
|
+
throw new Error(`Upload session chunk ${start}-${end} answered HTTP ${response.status}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
function createAttachmentUploadHandler(deps) {
|
|
128
|
+
return async (req, res) => {
|
|
129
|
+
if (req.method !== "PUT") {
|
|
130
|
+
res.setHeader("allow", "GET, PUT");
|
|
131
|
+
res.status(405).type("text/plain").send("Method not allowed");
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
const raw = req.query[TICKET_PARAM];
|
|
135
|
+
if (typeof raw !== "string" || raw.length === 0) {
|
|
136
|
+
refuse(res);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
const ticket = deps.store.redeem(raw);
|
|
140
|
+
if (!ticket || ticket.purpose !== "upload") {
|
|
141
|
+
refuse(res);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
if (!isPlainGraphPath(ticket.target)) {
|
|
145
|
+
logger.error("Attachment upload refused: ticket target is not a plain Graph path");
|
|
146
|
+
refuse(res);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
const declared = Number(req.headers["content-length"]);
|
|
150
|
+
if (!Number.isInteger(declared) || declared < 0) {
|
|
151
|
+
res.status(411).type("text/plain").send("Content-Length required");
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
if (declared === 0) {
|
|
155
|
+
res.status(400).type("text/plain").send("Empty upload");
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
if (declared > MAX_UPLOAD_BYTES) {
|
|
159
|
+
res.status(413).type("text/plain").send(`Upload exceeds ${MAX_UPLOAD_BYTES} bytes`);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
const graphClient = deps.getGraphClient();
|
|
163
|
+
if (!graphClient) {
|
|
164
|
+
logger.error("Attachment upload failed: Graph client is not initialised");
|
|
165
|
+
res.status(503).type("text/plain").send("Service unavailable");
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
let accessToken;
|
|
169
|
+
try {
|
|
170
|
+
accessToken = ticket.kind === "request-token" ? ticket.accessToken : await deps.authManager.getTokenForAccount(ticket.accountName);
|
|
171
|
+
if (!accessToken) throw new Error("No access token for this ticket");
|
|
172
|
+
} catch (error) {
|
|
173
|
+
logger.error(`Attachment upload failed for ${ticket.target}: ${error.message}`);
|
|
174
|
+
res.status(502).type("text/plain").send("Upstream upload failed");
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
res.setHeader("cache-control", "no-store");
|
|
178
|
+
res.setHeader("x-content-type-options", "nosniff");
|
|
179
|
+
try {
|
|
180
|
+
if (declared < INLINE_UPLOAD_LIMIT) {
|
|
181
|
+
const chunks = [];
|
|
182
|
+
let received = 0;
|
|
183
|
+
for await (const chunk of req) {
|
|
184
|
+
received += chunk.length;
|
|
185
|
+
if (received > declared) throw new Error("Body exceeds Content-Length");
|
|
186
|
+
chunks.push(chunk);
|
|
187
|
+
}
|
|
188
|
+
if (received !== declared) throw new Error("Body shorter than Content-Length");
|
|
189
|
+
const created = await graphClient.makeRequest(ticket.target, {
|
|
190
|
+
method: "POST",
|
|
191
|
+
accessToken,
|
|
192
|
+
body: JSON.stringify({
|
|
193
|
+
"@odata.type": "#microsoft.graph.fileAttachment",
|
|
194
|
+
name: ticket.name,
|
|
195
|
+
contentType: ticket.contentType,
|
|
196
|
+
contentBytes: Buffer.concat(chunks).toString("base64")
|
|
197
|
+
})
|
|
198
|
+
});
|
|
199
|
+
res.status(201).json({ ok: true, name: ticket.name, size: declared, id: created?.id });
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
const session = await graphClient.makeRequest(`${ticket.target}/createUploadSession`, {
|
|
203
|
+
method: "POST",
|
|
204
|
+
accessToken,
|
|
205
|
+
body: JSON.stringify({
|
|
206
|
+
AttachmentItem: {
|
|
207
|
+
attachmentType: "file",
|
|
208
|
+
name: ticket.name,
|
|
209
|
+
size: declared,
|
|
210
|
+
contentType: ticket.contentType
|
|
211
|
+
}
|
|
212
|
+
})
|
|
213
|
+
});
|
|
214
|
+
if (!session?.uploadUrl) throw new Error("Upload session returned no uploadUrl");
|
|
215
|
+
let pending = [];
|
|
216
|
+
let pendingBytes = 0;
|
|
217
|
+
let offset = 0;
|
|
218
|
+
for await (const piece of req) {
|
|
219
|
+
pending.push(piece);
|
|
220
|
+
pendingBytes += piece.length;
|
|
221
|
+
if (offset + pendingBytes > declared) throw new Error("Body exceeds Content-Length");
|
|
222
|
+
while (pendingBytes >= UPLOAD_CHUNK_BYTES) {
|
|
223
|
+
const joined = Buffer.concat(pending);
|
|
224
|
+
const chunk = joined.subarray(0, UPLOAD_CHUNK_BYTES);
|
|
225
|
+
await putChunk(session.uploadUrl, chunk, offset, declared);
|
|
226
|
+
offset += chunk.length;
|
|
227
|
+
const rest = joined.subarray(UPLOAD_CHUNK_BYTES);
|
|
228
|
+
pending = rest.length ? [Buffer.from(rest)] : [];
|
|
229
|
+
pendingBytes = rest.length;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
if (pendingBytes > 0) {
|
|
233
|
+
await putChunk(session.uploadUrl, Buffer.concat(pending), offset, declared);
|
|
234
|
+
offset += pendingBytes;
|
|
235
|
+
}
|
|
236
|
+
if (offset !== declared) throw new Error("Body shorter than Content-Length");
|
|
237
|
+
res.status(201).json({ ok: true, name: ticket.name, size: declared });
|
|
238
|
+
} catch (error) {
|
|
239
|
+
logger.error(`Attachment upload failed for ${ticket.target}: ${error.message}`);
|
|
240
|
+
if (!res.headersSent) res.status(502).type("text/plain").send("Upstream upload failed");
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
}
|
|
107
244
|
export {
|
|
245
|
+
MAX_UPLOAD_BYTES,
|
|
108
246
|
createAttachmentHandler,
|
|
247
|
+
createAttachmentUploadHandler,
|
|
109
248
|
forceAttachment
|
|
110
249
|
};
|
package/dist/endpoints.json
CHANGED
|
@@ -197,6 +197,64 @@
|
|
|
197
197
|
"workScopes": ["Mail.Send.Shared"],
|
|
198
198
|
"llmTip": "Send an existing draft from a shared mailbox. No request body needed — just call with the message ID and the shared mailbox user-id. Draft must exist in the shared mailbox's Drafts folder. Requires Send As permission on the shared mailbox in Exchange."
|
|
199
199
|
},
|
|
200
|
+
{
|
|
201
|
+
"pathPattern": "/users/{user-id}/mailFolders",
|
|
202
|
+
"method": "get",
|
|
203
|
+
"toolName": "list-shared-mailbox-folders",
|
|
204
|
+
"descriptionOverride": "List the top-level mail folders in another user's mailbox or a shared mailbox. The user-id is the mailbox owner's ID or email address, and delegated access to that mailbox is required.",
|
|
205
|
+
"presets": ["work"],
|
|
206
|
+
"workScopes": ["Mail.Read.Shared"]
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
"pathPattern": "/users/{user-id}/mailFolders/{mailFolder-id}/childFolders",
|
|
210
|
+
"method": "get",
|
|
211
|
+
"toolName": "list-shared-mailbox-child-folders",
|
|
212
|
+
"descriptionOverride": "List the child folders of a mail folder in another user's mailbox or a shared mailbox. The user-id is the mailbox owner's ID or email address, and delegated access to that mailbox is required.",
|
|
213
|
+
"presets": ["work"],
|
|
214
|
+
"workScopes": ["Mail.Read.Shared"]
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
"pathPattern": "/users/{user-id}/mailFolders",
|
|
218
|
+
"method": "post",
|
|
219
|
+
"toolName": "create-shared-mailbox-folder",
|
|
220
|
+
"descriptionOverride": "Create a top-level mail folder in another user's mailbox or a shared mailbox. The user-id is the mailbox owner's ID or email address, and delegated access to that mailbox is required.",
|
|
221
|
+
"presets": ["work"],
|
|
222
|
+
"workScopes": ["Mail.ReadWrite.Shared"]
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
"pathPattern": "/users/{user-id}/mailFolders/{mailFolder-id}/childFolders",
|
|
226
|
+
"method": "post",
|
|
227
|
+
"toolName": "create-shared-mailbox-child-folder",
|
|
228
|
+
"descriptionOverride": "Create a subfolder inside a mail folder in another user's mailbox or a shared mailbox. The user-id is the mailbox owner's ID or email address, and delegated access to that mailbox is required.",
|
|
229
|
+
"presets": ["work"],
|
|
230
|
+
"workScopes": ["Mail.ReadWrite.Shared"]
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
"pathPattern": "/users/{user-id}/messages/{message-id}/attachments",
|
|
234
|
+
"method": "get",
|
|
235
|
+
"toolName": "list-shared-mailbox-attachments",
|
|
236
|
+
"descriptionOverride": "List the attachments on a message in another user's mailbox or a shared mailbox. The user-id is the mailbox owner's ID or email address, and delegated access to that mailbox is required.",
|
|
237
|
+
"presets": ["work"],
|
|
238
|
+
"workScopes": ["Mail.Read.Shared"],
|
|
239
|
+
"llmTip": "To download the bytes, call download-bytes or get-download-url with target=/users/{user-id}/messages/{message-id}/attachments/{attachment-id}/$value."
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
"pathPattern": "/users/{user-id}/messages/{message-id}/attachments",
|
|
243
|
+
"method": "post",
|
|
244
|
+
"toolName": "add-shared-mailbox-attachment",
|
|
245
|
+
"descriptionOverride": "Add an attachment under 3MB to a draft message in another user's mailbox or a shared mailbox. The user-id is the mailbox owner's ID or email address, and delegated access to that mailbox is required.",
|
|
246
|
+
"presets": ["work"],
|
|
247
|
+
"workScopes": ["Mail.ReadWrite.Shared"],
|
|
248
|
+
"llmTip": "Body requires @odata.type: {\"@odata.type\": \"#microsoft.graph.fileAttachment\", \"name\": \"file.pdf\", \"contentBytes\": \"<base64>\"}. Graph has no working path for 3MB and above here: upload sessions return 403 in a shared or delegated mailbox."
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
"pathPattern": "/users/{user-id}/messages/{message-id}/attachments/{attachment-id}",
|
|
252
|
+
"method": "delete",
|
|
253
|
+
"toolName": "delete-shared-mailbox-attachment",
|
|
254
|
+
"descriptionOverride": "Delete an attachment from a message in another user's mailbox or a shared mailbox. The user-id is the mailbox owner's ID or email address, and delegated access to that mailbox is required.",
|
|
255
|
+
"presets": ["work"],
|
|
256
|
+
"workScopes": ["Mail.ReadWrite.Shared"]
|
|
257
|
+
},
|
|
200
258
|
{
|
|
201
259
|
"pathPattern": "/users",
|
|
202
260
|
"method": "get",
|
|
@@ -2394,6 +2452,22 @@
|
|
|
2394
2452
|
"scopes": ["Calendars.ReadWrite"],
|
|
2395
2453
|
"llmTip": "Dismisses a triggered event reminder so it won't re-fire. No request body required. Pair with list-calendar-events or get-schedule to find active reminders."
|
|
2396
2454
|
},
|
|
2455
|
+
{
|
|
2456
|
+
"pathPattern": "/me/events/{event-id}/attachments",
|
|
2457
|
+
"method": "post",
|
|
2458
|
+
"toolName": "add-calendar-event-attachment",
|
|
2459
|
+
"presets": ["calendar", "outlook", "personal"],
|
|
2460
|
+
"scopes": ["Calendars.ReadWrite"],
|
|
2461
|
+
"llmTip": "Under 3MB only. Body requires @odata.type: {\"@odata.type\": \"#microsoft.graph.fileAttachment\", \"name\": \"file.pdf\", \"contentBytes\": \"<base64>\"}. At 3MB and above use create-calendar-event-attachment-upload-session."
|
|
2462
|
+
},
|
|
2463
|
+
{
|
|
2464
|
+
"pathPattern": "/me/events/{event-id}/attachments/createUploadSession",
|
|
2465
|
+
"method": "post",
|
|
2466
|
+
"toolName": "create-calendar-event-attachment-upload-session",
|
|
2467
|
+
"presets": ["calendar", "outlook", "personal"],
|
|
2468
|
+
"scopes": ["Calendars.ReadWrite"],
|
|
2469
|
+
"llmTip": "For attachments 3MB to 150MB; under 3MB use add-calendar-event-attachment. Body: { AttachmentItem: { attachmentType: 'file', name: 'report.pdf', size: 5000000 } }. Returns a pre-authenticated uploadUrl; the caller PUTs the bytes there itself in ranges up to 4MB."
|
|
2470
|
+
},
|
|
2397
2471
|
{
|
|
2398
2472
|
"pathPattern": "/me/events/delta()",
|
|
2399
2473
|
"method": "get",
|
package/dist/generated/client.js
CHANGED
|
@@ -2774,6 +2774,20 @@ const microsoft_graph_driveCollectionResponse = z.object({
|
|
|
2774
2774
|
"@odata.nextLink": z.string().nullable(),
|
|
2775
2775
|
value: z.array(microsoft_graph_drive)
|
|
2776
2776
|
}).partial().passthrough();
|
|
2777
|
+
const microsoft_graph_attachmentType = z.enum(["file", "item", "reference"]);
|
|
2778
|
+
const microsoft_graph_attachmentItem = z.object({
|
|
2779
|
+
attachmentType: microsoft_graph_attachmentType.optional(),
|
|
2780
|
+
contentId: z.string().describe(
|
|
2781
|
+
"The CID or Content-Id of the attachment for referencing for the in-line attachments using the <img src='cid:contentId'> tag in HTML messages. Optional."
|
|
2782
|
+
).nullish(),
|
|
2783
|
+
contentType: z.string().describe("The nature of the data in the attachment. Optional.").nullish(),
|
|
2784
|
+
isInline: z.boolean().describe("true if the attachment is an inline attachment; otherwise, false. Optional.").nullish(),
|
|
2785
|
+
name: z.string().describe(
|
|
2786
|
+
"The display name of the attachment. This can be a descriptive string and doesn't have to be the actual file name. Required."
|
|
2787
|
+
).nullish(),
|
|
2788
|
+
size: z.number().describe("The length of the attachment in bytes. Required.").nullish()
|
|
2789
|
+
}).passthrough();
|
|
2790
|
+
const create_calendar_event_attachment_upload_session_Body = z.object({ AttachmentItem: microsoft_graph_attachmentItem }).partial().passthrough();
|
|
2777
2791
|
const accept_calendar_event_Body = z.object({ SendResponse: z.boolean().nullable().default(false), Comment: z.string().nullable() }).partial().passthrough();
|
|
2778
2792
|
const decline_calendar_event_Body = z.object({
|
|
2779
2793
|
ProposedNewTime: z.union([microsoft_graph_timeSlot, z.object({}).partial().passthrough()]),
|
|
@@ -3299,20 +3313,6 @@ const microsoft_graph_attachmentCollectionResponse = z.object({
|
|
|
3299
3313
|
"@odata.nextLink": z.string().nullable(),
|
|
3300
3314
|
value: z.array(microsoft_graph_attachment)
|
|
3301
3315
|
}).partial().passthrough();
|
|
3302
|
-
const microsoft_graph_attachmentType = z.enum(["file", "item", "reference"]);
|
|
3303
|
-
const microsoft_graph_attachmentItem = z.object({
|
|
3304
|
-
attachmentType: microsoft_graph_attachmentType.optional(),
|
|
3305
|
-
contentId: z.string().describe(
|
|
3306
|
-
"The CID or Content-Id of the attachment for referencing for the in-line attachments using the <img src='cid:contentId'> tag in HTML messages. Optional."
|
|
3307
|
-
).nullish(),
|
|
3308
|
-
contentType: z.string().describe("The nature of the data in the attachment. Optional.").nullish(),
|
|
3309
|
-
isInline: z.boolean().describe("true if the attachment is an inline attachment; otherwise, false. Optional.").nullish(),
|
|
3310
|
-
name: z.string().describe(
|
|
3311
|
-
"The display name of the attachment. This can be a descriptive string and doesn't have to be the actual file name. Required."
|
|
3312
|
-
).nullish(),
|
|
3313
|
-
size: z.number().describe("The length of the attachment in bytes. Required.").nullish()
|
|
3314
|
-
}).passthrough();
|
|
3315
|
-
const create_mail_attachment_upload_session_Body = z.object({ AttachmentItem: microsoft_graph_attachmentItem }).partial().passthrough();
|
|
3316
3316
|
const create_forward_draft_Body = z.object({
|
|
3317
3317
|
ToRecipients: z.array(microsoft_graph_recipient),
|
|
3318
3318
|
Message: z.union([microsoft_graph_message, z.object({}).partial().passthrough()]),
|
|
@@ -5149,6 +5149,9 @@ const schemas = {
|
|
|
5149
5149
|
microsoft_graph_list,
|
|
5150
5150
|
microsoft_graph_drive,
|
|
5151
5151
|
microsoft_graph_driveCollectionResponse,
|
|
5152
|
+
microsoft_graph_attachmentType,
|
|
5153
|
+
microsoft_graph_attachmentItem,
|
|
5154
|
+
create_calendar_event_attachment_upload_session_Body,
|
|
5152
5155
|
accept_calendar_event_Body,
|
|
5153
5156
|
decline_calendar_event_Body,
|
|
5154
5157
|
forward_calendar_event_Body,
|
|
@@ -5200,9 +5203,6 @@ const schemas = {
|
|
|
5200
5203
|
microsoft_graph_messageRuleCollectionResponse,
|
|
5201
5204
|
microsoft_graph_messageCollectionResponse,
|
|
5202
5205
|
microsoft_graph_attachmentCollectionResponse,
|
|
5203
|
-
microsoft_graph_attachmentType,
|
|
5204
|
-
microsoft_graph_attachmentItem,
|
|
5205
|
-
create_mail_attachment_upload_session_Body,
|
|
5206
5206
|
create_forward_draft_Body,
|
|
5207
5207
|
create_reply_draft_Body,
|
|
5208
5208
|
microsoft_graph_attendeeBase,
|
|
@@ -9377,6 +9377,38 @@ open extensions or extended properties, and how to specify extended properties.`
|
|
|
9377
9377
|
],
|
|
9378
9378
|
response: z.void()
|
|
9379
9379
|
},
|
|
9380
|
+
{
|
|
9381
|
+
method: "post",
|
|
9382
|
+
path: "/me/events/:eventId/attachments",
|
|
9383
|
+
alias: "add-calendar-event-attachment",
|
|
9384
|
+
description: `Use this API to add an attachment to an existing event. This operation limits the size of the attachment you can add to under 3 MB. If an organizer adds an attachment to a meeting event, the organizer can subsequently update the event to send the attachment and update the event for each attendee as well.`,
|
|
9385
|
+
requestFormat: "json",
|
|
9386
|
+
parameters: [
|
|
9387
|
+
{
|
|
9388
|
+
name: "body",
|
|
9389
|
+
description: `New navigation property`,
|
|
9390
|
+
type: "Body",
|
|
9391
|
+
schema: microsoft_graph_attachment
|
|
9392
|
+
}
|
|
9393
|
+
],
|
|
9394
|
+
response: z.void()
|
|
9395
|
+
},
|
|
9396
|
+
{
|
|
9397
|
+
method: "post",
|
|
9398
|
+
path: "/me/events/:eventId/attachments/createUploadSession",
|
|
9399
|
+
alias: "create-calendar-event-attachment-upload-session",
|
|
9400
|
+
description: `Create an upload session that allows an app to iteratively upload ranges of a file, so as to attach the file to the specified Outlook item. The item can be a message or event. Use this approach to attach a file if the file size is between 3 MB and 150 MB. To attach a file that's smaller than 3 MB, do a POST operation on the attachments navigation property of the Outlook item; see how to do this for a message or for an event. As part of the response, this action returns an upload URL that you can use in subsequent sequential PUT queries. Request headers for each PUT operation let you specify the exact range of bytes to be uploaded. This allows transfer to be resumed, in case the network connection is dropped during upload. The following are the steps to attach a file to an Outlook item using an upload session: See attach large files to Outlook messages or events for an example.`,
|
|
9401
|
+
requestFormat: "json",
|
|
9402
|
+
parameters: [
|
|
9403
|
+
{
|
|
9404
|
+
name: "body",
|
|
9405
|
+
description: `Action parameters`,
|
|
9406
|
+
type: "Body",
|
|
9407
|
+
schema: create_calendar_event_attachment_upload_session_Body
|
|
9408
|
+
}
|
|
9409
|
+
],
|
|
9410
|
+
response: z.void()
|
|
9411
|
+
},
|
|
9380
9412
|
{
|
|
9381
9413
|
method: "post",
|
|
9382
9414
|
path: "/me/events/:eventId/cancel",
|
|
@@ -10591,7 +10623,7 @@ resource.`,
|
|
|
10591
10623
|
name: "body",
|
|
10592
10624
|
description: `Action parameters`,
|
|
10593
10625
|
type: "Body",
|
|
10594
|
-
schema:
|
|
10626
|
+
schema: create_calendar_event_attachment_upload_session_Body
|
|
10595
10627
|
}
|
|
10596
10628
|
],
|
|
10597
10629
|
response: z.void()
|
|
@@ -15013,6 +15045,148 @@ Only the policyViolation property of a chatMessage can be updated in application
|
|
|
15013
15045
|
],
|
|
15014
15046
|
response: z.void()
|
|
15015
15047
|
},
|
|
15048
|
+
{
|
|
15049
|
+
method: "get",
|
|
15050
|
+
path: "/users/:userId/mailFolders",
|
|
15051
|
+
alias: "list-shared-mailbox-folders",
|
|
15052
|
+
description: `The user's mail folders. Read-only. Nullable.`,
|
|
15053
|
+
requestFormat: "json",
|
|
15054
|
+
parameters: [
|
|
15055
|
+
{
|
|
15056
|
+
name: "includeHiddenFolders",
|
|
15057
|
+
type: "Query",
|
|
15058
|
+
schema: z.string().describe("Include Hidden Folders").optional()
|
|
15059
|
+
},
|
|
15060
|
+
{
|
|
15061
|
+
name: "$top",
|
|
15062
|
+
type: "Query",
|
|
15063
|
+
schema: z.number().int().gte(0).describe("Show only the first n items").optional()
|
|
15064
|
+
},
|
|
15065
|
+
{
|
|
15066
|
+
name: "$skip",
|
|
15067
|
+
type: "Query",
|
|
15068
|
+
schema: z.number().int().gte(0).describe("Skip the first n items").optional()
|
|
15069
|
+
},
|
|
15070
|
+
{
|
|
15071
|
+
name: "$search",
|
|
15072
|
+
type: "Query",
|
|
15073
|
+
schema: z.string().describe("Search items by search phrases").optional()
|
|
15074
|
+
},
|
|
15075
|
+
{
|
|
15076
|
+
name: "$filter",
|
|
15077
|
+
type: "Query",
|
|
15078
|
+
schema: z.string().describe("Filter items by property values").optional()
|
|
15079
|
+
},
|
|
15080
|
+
{
|
|
15081
|
+
name: "$count",
|
|
15082
|
+
type: "Query",
|
|
15083
|
+
schema: z.boolean().describe("Include count of items").optional()
|
|
15084
|
+
},
|
|
15085
|
+
{
|
|
15086
|
+
name: "$orderby",
|
|
15087
|
+
type: "Query",
|
|
15088
|
+
schema: z.array(z.string()).describe("Order items by property values").optional()
|
|
15089
|
+
},
|
|
15090
|
+
{
|
|
15091
|
+
name: "$select",
|
|
15092
|
+
type: "Query",
|
|
15093
|
+
schema: z.array(z.string()).describe("Select properties to be returned").optional()
|
|
15094
|
+
},
|
|
15095
|
+
{
|
|
15096
|
+
name: "$expand",
|
|
15097
|
+
type: "Query",
|
|
15098
|
+
schema: z.array(z.string()).describe("Expand related entities").optional()
|
|
15099
|
+
}
|
|
15100
|
+
],
|
|
15101
|
+
response: z.void()
|
|
15102
|
+
},
|
|
15103
|
+
{
|
|
15104
|
+
method: "post",
|
|
15105
|
+
path: "/users/:userId/mailFolders",
|
|
15106
|
+
alias: "create-shared-mailbox-folder",
|
|
15107
|
+
description: `Create a shared mailbox folder.`,
|
|
15108
|
+
requestFormat: "json",
|
|
15109
|
+
parameters: [
|
|
15110
|
+
{
|
|
15111
|
+
name: "body",
|
|
15112
|
+
description: `New navigation property`,
|
|
15113
|
+
type: "Body",
|
|
15114
|
+
schema: microsoft_graph_mailFolder
|
|
15115
|
+
}
|
|
15116
|
+
],
|
|
15117
|
+
response: z.void()
|
|
15118
|
+
},
|
|
15119
|
+
{
|
|
15120
|
+
method: "get",
|
|
15121
|
+
path: "/users/:userId/mailFolders/:mailFolderId/childFolders",
|
|
15122
|
+
alias: "list-shared-mailbox-child-folders",
|
|
15123
|
+
description: `The collection of child folders in the mailFolder.`,
|
|
15124
|
+
requestFormat: "json",
|
|
15125
|
+
parameters: [
|
|
15126
|
+
{
|
|
15127
|
+
name: "includeHiddenFolders",
|
|
15128
|
+
type: "Query",
|
|
15129
|
+
schema: z.string().describe("Include Hidden Folders").optional()
|
|
15130
|
+
},
|
|
15131
|
+
{
|
|
15132
|
+
name: "$top",
|
|
15133
|
+
type: "Query",
|
|
15134
|
+
schema: z.number().int().gte(0).describe("Show only the first n items").optional()
|
|
15135
|
+
},
|
|
15136
|
+
{
|
|
15137
|
+
name: "$skip",
|
|
15138
|
+
type: "Query",
|
|
15139
|
+
schema: z.number().int().gte(0).describe("Skip the first n items").optional()
|
|
15140
|
+
},
|
|
15141
|
+
{
|
|
15142
|
+
name: "$search",
|
|
15143
|
+
type: "Query",
|
|
15144
|
+
schema: z.string().describe("Search items by search phrases").optional()
|
|
15145
|
+
},
|
|
15146
|
+
{
|
|
15147
|
+
name: "$filter",
|
|
15148
|
+
type: "Query",
|
|
15149
|
+
schema: z.string().describe("Filter items by property values").optional()
|
|
15150
|
+
},
|
|
15151
|
+
{
|
|
15152
|
+
name: "$count",
|
|
15153
|
+
type: "Query",
|
|
15154
|
+
schema: z.boolean().describe("Include count of items").optional()
|
|
15155
|
+
},
|
|
15156
|
+
{
|
|
15157
|
+
name: "$orderby",
|
|
15158
|
+
type: "Query",
|
|
15159
|
+
schema: z.array(z.string()).describe("Order items by property values").optional()
|
|
15160
|
+
},
|
|
15161
|
+
{
|
|
15162
|
+
name: "$select",
|
|
15163
|
+
type: "Query",
|
|
15164
|
+
schema: z.array(z.string()).describe("Select properties to be returned").optional()
|
|
15165
|
+
},
|
|
15166
|
+
{
|
|
15167
|
+
name: "$expand",
|
|
15168
|
+
type: "Query",
|
|
15169
|
+
schema: z.array(z.string()).describe("Expand related entities").optional()
|
|
15170
|
+
}
|
|
15171
|
+
],
|
|
15172
|
+
response: z.void()
|
|
15173
|
+
},
|
|
15174
|
+
{
|
|
15175
|
+
method: "post",
|
|
15176
|
+
path: "/users/:userId/mailFolders/:mailFolderId/childFolders",
|
|
15177
|
+
alias: "create-shared-mailbox-child-folder",
|
|
15178
|
+
description: `Create a shared mailbox child folder.`,
|
|
15179
|
+
requestFormat: "json",
|
|
15180
|
+
parameters: [
|
|
15181
|
+
{
|
|
15182
|
+
name: "body",
|
|
15183
|
+
description: `New navigation property`,
|
|
15184
|
+
type: "Body",
|
|
15185
|
+
schema: microsoft_graph_mailFolder
|
|
15186
|
+
}
|
|
15187
|
+
],
|
|
15188
|
+
response: z.void()
|
|
15189
|
+
},
|
|
15016
15190
|
{
|
|
15017
15191
|
method: "get",
|
|
15018
15192
|
path: "/users/:userId/mailFolders/:mailFolderId/messages",
|
|
@@ -15294,6 +15468,87 @@ Only the policyViolation property of a chatMessage can be updated in application
|
|
|
15294
15468
|
],
|
|
15295
15469
|
response: z.void()
|
|
15296
15470
|
},
|
|
15471
|
+
{
|
|
15472
|
+
method: "get",
|
|
15473
|
+
path: "/users/:userId/messages/:messageId/attachments",
|
|
15474
|
+
alias: "list-shared-mailbox-attachments",
|
|
15475
|
+
description: `The fileAttachment and itemAttachment attachments for the message.`,
|
|
15476
|
+
requestFormat: "json",
|
|
15477
|
+
parameters: [
|
|
15478
|
+
{
|
|
15479
|
+
name: "$top",
|
|
15480
|
+
type: "Query",
|
|
15481
|
+
schema: z.number().int().gte(0).describe("Show only the first n items").optional()
|
|
15482
|
+
},
|
|
15483
|
+
{
|
|
15484
|
+
name: "$skip",
|
|
15485
|
+
type: "Query",
|
|
15486
|
+
schema: z.number().int().gte(0).describe("Skip the first n items").optional()
|
|
15487
|
+
},
|
|
15488
|
+
{
|
|
15489
|
+
name: "$search",
|
|
15490
|
+
type: "Query",
|
|
15491
|
+
schema: z.string().describe("Search items by search phrases").optional()
|
|
15492
|
+
},
|
|
15493
|
+
{
|
|
15494
|
+
name: "$filter",
|
|
15495
|
+
type: "Query",
|
|
15496
|
+
schema: z.string().describe("Filter items by property values").optional()
|
|
15497
|
+
},
|
|
15498
|
+
{
|
|
15499
|
+
name: "$count",
|
|
15500
|
+
type: "Query",
|
|
15501
|
+
schema: z.boolean().describe("Include count of items").optional()
|
|
15502
|
+
},
|
|
15503
|
+
{
|
|
15504
|
+
name: "$orderby",
|
|
15505
|
+
type: "Query",
|
|
15506
|
+
schema: z.array(z.string()).describe("Order items by property values").optional()
|
|
15507
|
+
},
|
|
15508
|
+
{
|
|
15509
|
+
name: "$select",
|
|
15510
|
+
type: "Query",
|
|
15511
|
+
schema: z.array(z.string()).describe("Select properties to be returned").optional()
|
|
15512
|
+
},
|
|
15513
|
+
{
|
|
15514
|
+
name: "$expand",
|
|
15515
|
+
type: "Query",
|
|
15516
|
+
schema: z.array(z.string()).describe("Expand related entities").optional()
|
|
15517
|
+
}
|
|
15518
|
+
],
|
|
15519
|
+
response: z.void()
|
|
15520
|
+
},
|
|
15521
|
+
{
|
|
15522
|
+
method: "post",
|
|
15523
|
+
path: "/users/:userId/messages/:messageId/attachments",
|
|
15524
|
+
alias: "add-shared-mailbox-attachment",
|
|
15525
|
+
description: `Add a shared mailbox attachment.`,
|
|
15526
|
+
requestFormat: "json",
|
|
15527
|
+
parameters: [
|
|
15528
|
+
{
|
|
15529
|
+
name: "body",
|
|
15530
|
+
description: `New navigation property`,
|
|
15531
|
+
type: "Body",
|
|
15532
|
+
schema: microsoft_graph_attachment
|
|
15533
|
+
}
|
|
15534
|
+
],
|
|
15535
|
+
response: z.void()
|
|
15536
|
+
},
|
|
15537
|
+
{
|
|
15538
|
+
method: "delete",
|
|
15539
|
+
path: "/users/:userId/messages/:messageId/attachments/:attachmentId",
|
|
15540
|
+
alias: "delete-shared-mailbox-attachment",
|
|
15541
|
+
description: `Delete a shared mailbox attachment.`,
|
|
15542
|
+
requestFormat: "json",
|
|
15543
|
+
parameters: [
|
|
15544
|
+
{
|
|
15545
|
+
name: "If-Match",
|
|
15546
|
+
type: "Header",
|
|
15547
|
+
schema: z.string().describe("ETag").optional()
|
|
15548
|
+
}
|
|
15549
|
+
],
|
|
15550
|
+
response: z.void()
|
|
15551
|
+
},
|
|
15297
15552
|
{
|
|
15298
15553
|
method: "post",
|
|
15299
15554
|
path: "/users/:userId/messages/:messageId/createForward",
|
package/dist/graph-tools.js
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
import { api as betaApi } from "./generated/client-beta.js";
|
|
16
16
|
import { api } from "./generated/client.js";
|
|
17
17
|
import { getAttachmentMinting } from "./lib/attachment-minting.js";
|
|
18
|
+
import { MAX_UPLOAD_BYTES } from "./attachment-route.js";
|
|
18
19
|
import {
|
|
19
20
|
buildAttachmentUrl,
|
|
20
21
|
isPlainGraphPath,
|
|
@@ -545,6 +546,65 @@ async function checkAccountParamInBearerMode(accountParam, authManager) {
|
|
|
545
546
|
if (bearerIdentity && bearerIdentity.toLowerCase() === accountParam.toLowerCase()) return null;
|
|
546
547
|
return `The 'account' parameter is not supported in HTTP/OAuth mode: every request uses the identity of the connecting client's bearer token` + (bearerIdentity ? ` ('${bearerIdentity}')` : "") + `, so account switching is not possible. To act as '${accountParam}', reconnect the MCP client authenticated as that account, or run the server in stdio mode (or HTTP with --trust-proxy-auth) where cached accounts are available.`;
|
|
547
548
|
}
|
|
549
|
+
const UPLOAD_TARGET = /^(\/me|\/users\/[^/]+)\/(messages|events)\/[^/]+\/attachments$/;
|
|
550
|
+
const UPLOAD_CONTENT_TYPE = /^[\w.+-]+\/[\w.+-]+$/;
|
|
551
|
+
function uploadError(message) {
|
|
552
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: message }) }], isError: true };
|
|
553
|
+
}
|
|
554
|
+
async function mintUploadUrl(target, name, contentType, accountParam, authManager) {
|
|
555
|
+
const minting = getAttachmentMinting();
|
|
556
|
+
if (!minting) {
|
|
557
|
+
const smallFileTool = target.includes("/events/") ? "add-calendar-event-attachment" : target.startsWith("/users/") ? "add-shared-mailbox-attachment" : "add-mail-attachment";
|
|
558
|
+
return uploadError(
|
|
559
|
+
`This server is not running with --enable-attachment-urls, so no upload URL can be minted. Attach small files with ${smallFileTool} (base64 contentBytes).`
|
|
560
|
+
);
|
|
561
|
+
}
|
|
562
|
+
if (!UPLOAD_TARGET.test(target) || !isPlainGraphPath(target)) {
|
|
563
|
+
return uploadError(
|
|
564
|
+
"target must be the attachments collection of a draft message or an event: /me/messages/{message-id}/attachments or /me/events/{event-id}/attachments."
|
|
565
|
+
);
|
|
566
|
+
}
|
|
567
|
+
const cleanName = Array.from(name.trim()).filter((char) => {
|
|
568
|
+
const code = char.charCodeAt(0);
|
|
569
|
+
return code > 31 && code !== 127;
|
|
570
|
+
}).join("").slice(0, 255);
|
|
571
|
+
if (!cleanName) return uploadError("name is required.");
|
|
572
|
+
if (!UPLOAD_CONTENT_TYPE.test(contentType)) {
|
|
573
|
+
return uploadError("contentType must be a MIME type such as application/pdf.");
|
|
574
|
+
}
|
|
575
|
+
const identityFromRequest = Boolean(authManager?.isOAuthModeEnabled() || getRequestTokens());
|
|
576
|
+
const requestToken = identityFromRequest ? getRequestTokens()?.accessToken ?? await authManager?.getToken().catch(() => null) ?? void 0 : void 0;
|
|
577
|
+
if (identityFromRequest && !requestToken) {
|
|
578
|
+
return uploadError(
|
|
579
|
+
"No Graph access token is available for this request, so no upload URL can be minted."
|
|
580
|
+
);
|
|
581
|
+
}
|
|
582
|
+
const accountModeError = await checkAccountParamInBearerMode(accountParam, authManager);
|
|
583
|
+
if (accountModeError) return uploadError(accountModeError);
|
|
584
|
+
let ticket;
|
|
585
|
+
try {
|
|
586
|
+
const upload = { name: cleanName, contentType };
|
|
587
|
+
ticket = requestToken ? minting.store.mintUploadWithToken(target, requestToken, upload) : minting.store.mintUpload(target, accountParam, upload);
|
|
588
|
+
} catch (error) {
|
|
589
|
+
if (error instanceof TicketStoreFullError) return uploadError(error.message);
|
|
590
|
+
throw error;
|
|
591
|
+
}
|
|
592
|
+
return {
|
|
593
|
+
content: [
|
|
594
|
+
{
|
|
595
|
+
type: "text",
|
|
596
|
+
text: JSON.stringify({
|
|
597
|
+
uploadUrl: buildAttachmentUrl(minting.config, ticket.id),
|
|
598
|
+
method: "PUT",
|
|
599
|
+
expiresAt: new Date(ticket.expiresAtMs).toISOString(),
|
|
600
|
+
singleUse: true,
|
|
601
|
+
maxBytes: MAX_UPLOAD_BYTES,
|
|
602
|
+
note: 'PUT the raw file bytes with a Content-Length (e.g. curl -T <file> "<uploadUrl>"). Served by this server; valid for one upload until it expires.'
|
|
603
|
+
})
|
|
604
|
+
}
|
|
605
|
+
]
|
|
606
|
+
};
|
|
607
|
+
}
|
|
548
608
|
async function mintDownloadUrl(target, accountParam, authManager) {
|
|
549
609
|
const minting = getAttachmentMinting();
|
|
550
610
|
if (!minting) return null;
|
|
@@ -610,6 +670,50 @@ async function mintDownloadUrl(target, accountParam, authManager) {
|
|
|
610
670
|
};
|
|
611
671
|
}
|
|
612
672
|
const UTILITY_TOOLS = [
|
|
673
|
+
{
|
|
674
|
+
name: "get-upload-url",
|
|
675
|
+
method: "PUT",
|
|
676
|
+
path: "tool:get-upload-url",
|
|
677
|
+
searchKeywords: "upload attachment attach file draft email attach file event large attachment send file out-of-band upload url",
|
|
678
|
+
description: 'Mint a short-lived, single-use URL that this server serves for uploading a file as an attachment to a draft message or an event, so the bytes go straight from the caller to Microsoft Graph instead of passing as base64 through the agent context. target is the attachments collection of the item: /me/messages/{message-id}/attachments (a draft; create it first) or /me/events/{event-id}/attachments. PUT the raw file bytes to the returned uploadUrl with a Content-Length (e.g. curl -T file "<uploadUrl>"); the response confirms the attachment. Files under 3 MB are attached directly, larger ones (up to 150 MB) through a Graph upload session. Requires --enable-attachment-urls. Returns { uploadUrl, method, expiresAt, singleUse, maxBytes }.',
|
|
679
|
+
readOnlyHint: false,
|
|
680
|
+
openWorldHint: true,
|
|
681
|
+
buildSchema: (ctx) => {
|
|
682
|
+
const schema = {
|
|
683
|
+
target: z.string().describe(
|
|
684
|
+
"Attachments collection of a draft message or an event, e.g. /me/messages/{message-id}/attachments or /me/events/{event-id}/attachments."
|
|
685
|
+
),
|
|
686
|
+
name: z.string().describe("Attachment file name as the recipient will see it, e.g. report.pdf"),
|
|
687
|
+
contentType: z.string().optional().describe(
|
|
688
|
+
"MIME type of the file, e.g. application/pdf. Defaults to application/octet-stream."
|
|
689
|
+
)
|
|
690
|
+
};
|
|
691
|
+
if (ctx.multiAccount) {
|
|
692
|
+
schema["account"] = z.string().optional().describe(
|
|
693
|
+
"Account to use when multiple Microsoft accounts are configured. Required when multiple accounts exist (see list-accounts)."
|
|
694
|
+
);
|
|
695
|
+
}
|
|
696
|
+
return schema;
|
|
697
|
+
},
|
|
698
|
+
execute: async (params, { authManager }) => {
|
|
699
|
+
const target = params.target;
|
|
700
|
+
const name = params.name;
|
|
701
|
+
if (typeof target !== "string" || target.length === 0) {
|
|
702
|
+
return uploadError("target is required and must be a non-empty string.");
|
|
703
|
+
}
|
|
704
|
+
if (typeof name !== "string" || name.length === 0) {
|
|
705
|
+
return uploadError("name is required and must be a non-empty string.");
|
|
706
|
+
}
|
|
707
|
+
const contentType = typeof params.contentType === "string" && params.contentType.length > 0 ? params.contentType : "application/octet-stream";
|
|
708
|
+
return mintUploadUrl(
|
|
709
|
+
target,
|
|
710
|
+
name,
|
|
711
|
+
contentType,
|
|
712
|
+
params.account,
|
|
713
|
+
authManager
|
|
714
|
+
);
|
|
715
|
+
}
|
|
716
|
+
},
|
|
613
717
|
{
|
|
614
718
|
name: "parse-teams-url",
|
|
615
719
|
method: "POST",
|
|
@@ -53,14 +53,14 @@ class AttachmentTicketStore {
|
|
|
53
53
|
if (ticket.expiresAtMs <= nowMs) this.tickets.delete(id);
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
|
-
add(target, identity, nowMs) {
|
|
56
|
+
add(target, identity, nowMs, purpose = { purpose: "download" }) {
|
|
57
57
|
this.sweep(nowMs);
|
|
58
58
|
if (this.tickets.size >= MAX_LIVE_TICKETS) {
|
|
59
59
|
throw new TicketStoreFullError(MAX_LIVE_TICKETS);
|
|
60
60
|
}
|
|
61
61
|
const id = randomBytes(TICKET_BYTES).toString("base64url");
|
|
62
62
|
const expiresAtMs = nowMs + this.ttlSeconds * 1e3;
|
|
63
|
-
this.tickets.set(id, { ...identity, target, expiresAtMs });
|
|
63
|
+
this.tickets.set(id, { ...identity, ...purpose, target, expiresAtMs });
|
|
64
64
|
return { id, expiresAtMs };
|
|
65
65
|
}
|
|
66
66
|
/** Mint a ticket redeemed with this server's own token for `accountName`. */
|
|
@@ -72,6 +72,21 @@ class AttachmentTicketStore {
|
|
|
72
72
|
if (!accessToken) throw new Error("A request-token ticket needs a token");
|
|
73
73
|
return this.add(target, { kind: "request-token", accessToken }, nowMs);
|
|
74
74
|
}
|
|
75
|
+
/** Mint an upload ticket attached as this server's own token for `accountName`. */
|
|
76
|
+
mintUpload(target, accountName, upload, nowMs = Date.now()) {
|
|
77
|
+
return this.add(target, { kind: "server-account", accountName }, nowMs, {
|
|
78
|
+
purpose: "upload",
|
|
79
|
+
...upload
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
/** Mint an upload ticket attached with `accessToken` and nothing else. */
|
|
83
|
+
mintUploadWithToken(target, accessToken, upload, nowMs = Date.now()) {
|
|
84
|
+
if (!accessToken) throw new Error("A request-token ticket needs a token");
|
|
85
|
+
return this.add(target, { kind: "request-token", accessToken }, nowMs, {
|
|
86
|
+
purpose: "upload",
|
|
87
|
+
...upload
|
|
88
|
+
});
|
|
89
|
+
}
|
|
75
90
|
/**
|
|
76
91
|
* Return the ticket and burn it, or undefined.
|
|
77
92
|
*
|
package/dist/server.js
CHANGED
|
@@ -8,7 +8,7 @@ import rateLimit from "express-rate-limit";
|
|
|
8
8
|
import helmet from "helmet";
|
|
9
9
|
import crypto from "node:crypto";
|
|
10
10
|
import { isIP, isIPv6 } from "node:net";
|
|
11
|
-
import { createAttachmentHandler } from "./attachment-route.js";
|
|
11
|
+
import { createAttachmentHandler, createAttachmentUploadHandler } from "./attachment-route.js";
|
|
12
12
|
import { registerAuthTools } from "./auth-tools.js";
|
|
13
13
|
import {
|
|
14
14
|
buildScopesFromEndpoints,
|
|
@@ -722,14 +722,13 @@ class MicrosoftGraphServer {
|
|
|
722
722
|
})
|
|
723
723
|
);
|
|
724
724
|
}
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
);
|
|
725
|
+
const attachmentDeps = {
|
|
726
|
+
store: ticketStore,
|
|
727
|
+
getGraphClient: () => this.graphClient,
|
|
728
|
+
authManager: this.authManager
|
|
729
|
+
};
|
|
730
|
+
attachmentApp.get(ATTACHMENT_ROUTE, createAttachmentHandler(attachmentDeps));
|
|
731
|
+
attachmentApp.put(ATTACHMENT_ROUTE, createAttachmentUploadHandler(attachmentDeps));
|
|
733
732
|
logger.info(
|
|
734
733
|
` - Attachment URLs: ${attachmentConfig.base}${ATTACHMENT_ROUTE} (ttl ${attachmentConfig.ttlSeconds}s, key id ${attachmentConfig.keyId})`
|
|
735
734
|
);
|
package/dist/tool-categories.js
CHANGED
|
@@ -62,6 +62,7 @@ const PRESET_META = {
|
|
|
62
62
|
const UNIVERSAL_UTILITY_TOOLS = ["download-bytes", "download-bytes-to-file"];
|
|
63
63
|
const SCOPED_UTILITY_TOOLS = {
|
|
64
64
|
"get-download-url": ["files", "onedrive", "personal", "work", "search"],
|
|
65
|
+
"get-upload-url": ["mail", "calendar", "outlook", "personal", "work"],
|
|
65
66
|
"parse-teams-url": ["teams", "teams-write", "work"]
|
|
66
67
|
};
|
|
67
68
|
for (const [tool, presets] of Object.entries(SCOPED_UTILITY_TOOLS)) {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@softeria/ms-365-mcp-server",
|
|
3
3
|
"mcpName": "io.github.Softeria/ms-365-mcp-server",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.161.0",
|
|
5
5
|
"description": " A Model Context Protocol (MCP) server for interacting with Microsoft 365 and Office services through the Graph API",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "dist/index.js",
|
package/src/endpoints.json
CHANGED
|
@@ -197,6 +197,64 @@
|
|
|
197
197
|
"workScopes": ["Mail.Send.Shared"],
|
|
198
198
|
"llmTip": "Send an existing draft from a shared mailbox. No request body needed — just call with the message ID and the shared mailbox user-id. Draft must exist in the shared mailbox's Drafts folder. Requires Send As permission on the shared mailbox in Exchange."
|
|
199
199
|
},
|
|
200
|
+
{
|
|
201
|
+
"pathPattern": "/users/{user-id}/mailFolders",
|
|
202
|
+
"method": "get",
|
|
203
|
+
"toolName": "list-shared-mailbox-folders",
|
|
204
|
+
"descriptionOverride": "List the top-level mail folders in another user's mailbox or a shared mailbox. The user-id is the mailbox owner's ID or email address, and delegated access to that mailbox is required.",
|
|
205
|
+
"presets": ["work"],
|
|
206
|
+
"workScopes": ["Mail.Read.Shared"]
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
"pathPattern": "/users/{user-id}/mailFolders/{mailFolder-id}/childFolders",
|
|
210
|
+
"method": "get",
|
|
211
|
+
"toolName": "list-shared-mailbox-child-folders",
|
|
212
|
+
"descriptionOverride": "List the child folders of a mail folder in another user's mailbox or a shared mailbox. The user-id is the mailbox owner's ID or email address, and delegated access to that mailbox is required.",
|
|
213
|
+
"presets": ["work"],
|
|
214
|
+
"workScopes": ["Mail.Read.Shared"]
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
"pathPattern": "/users/{user-id}/mailFolders",
|
|
218
|
+
"method": "post",
|
|
219
|
+
"toolName": "create-shared-mailbox-folder",
|
|
220
|
+
"descriptionOverride": "Create a top-level mail folder in another user's mailbox or a shared mailbox. The user-id is the mailbox owner's ID or email address, and delegated access to that mailbox is required.",
|
|
221
|
+
"presets": ["work"],
|
|
222
|
+
"workScopes": ["Mail.ReadWrite.Shared"]
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
"pathPattern": "/users/{user-id}/mailFolders/{mailFolder-id}/childFolders",
|
|
226
|
+
"method": "post",
|
|
227
|
+
"toolName": "create-shared-mailbox-child-folder",
|
|
228
|
+
"descriptionOverride": "Create a subfolder inside a mail folder in another user's mailbox or a shared mailbox. The user-id is the mailbox owner's ID or email address, and delegated access to that mailbox is required.",
|
|
229
|
+
"presets": ["work"],
|
|
230
|
+
"workScopes": ["Mail.ReadWrite.Shared"]
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
"pathPattern": "/users/{user-id}/messages/{message-id}/attachments",
|
|
234
|
+
"method": "get",
|
|
235
|
+
"toolName": "list-shared-mailbox-attachments",
|
|
236
|
+
"descriptionOverride": "List the attachments on a message in another user's mailbox or a shared mailbox. The user-id is the mailbox owner's ID or email address, and delegated access to that mailbox is required.",
|
|
237
|
+
"presets": ["work"],
|
|
238
|
+
"workScopes": ["Mail.Read.Shared"],
|
|
239
|
+
"llmTip": "To download the bytes, call download-bytes or get-download-url with target=/users/{user-id}/messages/{message-id}/attachments/{attachment-id}/$value."
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
"pathPattern": "/users/{user-id}/messages/{message-id}/attachments",
|
|
243
|
+
"method": "post",
|
|
244
|
+
"toolName": "add-shared-mailbox-attachment",
|
|
245
|
+
"descriptionOverride": "Add an attachment under 3MB to a draft message in another user's mailbox or a shared mailbox. The user-id is the mailbox owner's ID or email address, and delegated access to that mailbox is required.",
|
|
246
|
+
"presets": ["work"],
|
|
247
|
+
"workScopes": ["Mail.ReadWrite.Shared"],
|
|
248
|
+
"llmTip": "Body requires @odata.type: {\"@odata.type\": \"#microsoft.graph.fileAttachment\", \"name\": \"file.pdf\", \"contentBytes\": \"<base64>\"}. Graph has no working path for 3MB and above here: upload sessions return 403 in a shared or delegated mailbox."
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
"pathPattern": "/users/{user-id}/messages/{message-id}/attachments/{attachment-id}",
|
|
252
|
+
"method": "delete",
|
|
253
|
+
"toolName": "delete-shared-mailbox-attachment",
|
|
254
|
+
"descriptionOverride": "Delete an attachment from a message in another user's mailbox or a shared mailbox. The user-id is the mailbox owner's ID or email address, and delegated access to that mailbox is required.",
|
|
255
|
+
"presets": ["work"],
|
|
256
|
+
"workScopes": ["Mail.ReadWrite.Shared"]
|
|
257
|
+
},
|
|
200
258
|
{
|
|
201
259
|
"pathPattern": "/users",
|
|
202
260
|
"method": "get",
|
|
@@ -2394,6 +2452,22 @@
|
|
|
2394
2452
|
"scopes": ["Calendars.ReadWrite"],
|
|
2395
2453
|
"llmTip": "Dismisses a triggered event reminder so it won't re-fire. No request body required. Pair with list-calendar-events or get-schedule to find active reminders."
|
|
2396
2454
|
},
|
|
2455
|
+
{
|
|
2456
|
+
"pathPattern": "/me/events/{event-id}/attachments",
|
|
2457
|
+
"method": "post",
|
|
2458
|
+
"toolName": "add-calendar-event-attachment",
|
|
2459
|
+
"presets": ["calendar", "outlook", "personal"],
|
|
2460
|
+
"scopes": ["Calendars.ReadWrite"],
|
|
2461
|
+
"llmTip": "Under 3MB only. Body requires @odata.type: {\"@odata.type\": \"#microsoft.graph.fileAttachment\", \"name\": \"file.pdf\", \"contentBytes\": \"<base64>\"}. At 3MB and above use create-calendar-event-attachment-upload-session."
|
|
2462
|
+
},
|
|
2463
|
+
{
|
|
2464
|
+
"pathPattern": "/me/events/{event-id}/attachments/createUploadSession",
|
|
2465
|
+
"method": "post",
|
|
2466
|
+
"toolName": "create-calendar-event-attachment-upload-session",
|
|
2467
|
+
"presets": ["calendar", "outlook", "personal"],
|
|
2468
|
+
"scopes": ["Calendars.ReadWrite"],
|
|
2469
|
+
"llmTip": "For attachments 3MB to 150MB; under 3MB use add-calendar-event-attachment. Body: { AttachmentItem: { attachmentType: 'file', name: 'report.pdf', size: 5000000 } }. Returns a pre-authenticated uploadUrl; the caller PUTs the bytes there itself in ranges up to 4MB."
|
|
2470
|
+
},
|
|
2397
2471
|
{
|
|
2398
2472
|
"pathPattern": "/me/events/delta()",
|
|
2399
2473
|
"method": "get",
|