@chrischall/office-outlook-mcp 0.1.5 → 0.2.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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/dist/bundle.js +93 -45
- package/dist/client.js +25 -7
- package/dist/tools/_paging.js +32 -0
- package/dist/tools/calendar.js +9 -9
- package/dist/tools/directory.js +11 -7
- package/dist/tools/mail.js +17 -14
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/server.json +2 -2
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
},
|
|
8
8
|
"metadata": {
|
|
9
9
|
"description": "MCP server for Outlook / Microsoft 365 — mail, calendar, contacts and tasks via natural language",
|
|
10
|
-
"version": "0.
|
|
10
|
+
"version": "0.2.0"
|
|
11
11
|
},
|
|
12
12
|
"plugins": [
|
|
13
13
|
{
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"displayName": "Outlook (Microsoft 365)",
|
|
16
16
|
"source": "./",
|
|
17
17
|
"description": "MCP server for Outlook / Microsoft 365 — read mail, folders, calendar, contacts and tasks, and send mail with confirmation",
|
|
18
|
-
"version": "0.
|
|
18
|
+
"version": "0.2.0",
|
|
19
19
|
"author": {
|
|
20
20
|
"name": "Chris Hall"
|
|
21
21
|
},
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "office-outlook-mcp",
|
|
3
3
|
"displayName": "Outlook (Microsoft 365)",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.2.0",
|
|
5
5
|
"description": "Outlook / Microsoft 365 for Claude — read mail, folders, calendar, contacts and tasks, and send mail with confirmation, using your signed-in browser session",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Chris Hall",
|
package/dist/bundle.js
CHANGED
|
@@ -36027,7 +36027,7 @@ var init_version = __esm({
|
|
|
36027
36027
|
"src/version.ts"() {
|
|
36028
36028
|
"use strict";
|
|
36029
36029
|
PACKAGE_NAME = "office-outlook-mcp";
|
|
36030
|
-
VERSION = "0.
|
|
36030
|
+
VERSION = "0.2.0";
|
|
36031
36031
|
}
|
|
36032
36032
|
});
|
|
36033
36033
|
|
|
@@ -46223,17 +46223,33 @@ var OutlookClient = class {
|
|
|
46223
46223
|
headers: prefer.length ? { Prefer: prefer.join(", ") } : void 0
|
|
46224
46224
|
});
|
|
46225
46225
|
}
|
|
46226
|
-
/**
|
|
46227
|
-
|
|
46226
|
+
/**
|
|
46227
|
+
* GET an absolute URL — used only to follow `@odata.nextLink`.
|
|
46228
|
+
*
|
|
46229
|
+
* The link is absolute and already carries the base path
|
|
46230
|
+
* (`https://outlook.office.com/api/v2.0/me/messages?$skip=25`), while the
|
|
46231
|
+
* underlying client appends every path to that same base. So the base path is
|
|
46232
|
+
* stripped here, and a link that is not under it — another origin, or another
|
|
46233
|
+
* path on the same host — is refused rather than sent the bearer token.
|
|
46234
|
+
*/
|
|
46235
|
+
async getAbsolute(url2, opts = {}) {
|
|
46228
46236
|
this.#requireConfigured();
|
|
46229
46237
|
const base = new URL(this.#baseUrl);
|
|
46230
|
-
|
|
46231
|
-
|
|
46232
|
-
|
|
46233
|
-
|
|
46238
|
+
let target;
|
|
46239
|
+
try {
|
|
46240
|
+
target = new URL(url2);
|
|
46241
|
+
} catch {
|
|
46242
|
+
throw new McpToolError(`Refusing to follow a link that is not a URL: ${url2}`, {
|
|
46243
|
+
hint: "Pass the nextLink value exactly as a previous listing returned it."
|
|
46244
|
+
});
|
|
46245
|
+
}
|
|
46246
|
+
const prefix = base.pathname.replace(/\/+$/, "");
|
|
46247
|
+
if (target.origin !== base.origin || !(target.pathname === prefix || target.pathname.startsWith(`${prefix}/`))) {
|
|
46248
|
+
throw new McpToolError(`Refusing to follow a link to ${target.origin}${target.pathname}.`, {
|
|
46249
|
+
hint: `Pagination links must stay under the Outlook API base (${this.#baseUrl}).`
|
|
46234
46250
|
});
|
|
46235
46251
|
}
|
|
46236
|
-
return this
|
|
46252
|
+
return this.get(target.pathname.slice(prefix.length) + target.search, opts);
|
|
46237
46253
|
}
|
|
46238
46254
|
/**
|
|
46239
46255
|
* The single write path. Everything mutating goes through here so auth and
|
|
@@ -46373,6 +46389,22 @@ function projectCollection(payload, project, label) {
|
|
|
46373
46389
|
});
|
|
46374
46390
|
}
|
|
46375
46391
|
|
|
46392
|
+
// src/tools/_paging.ts
|
|
46393
|
+
init_zod();
|
|
46394
|
+
var nextLinkParam = external_exports.string().min(1).optional().describe(
|
|
46395
|
+
"Continue a previous listing: pass its `nextLink` value unchanged to fetch the next page. When set, the paging/filter arguments are taken from the link and ignored here."
|
|
46396
|
+
);
|
|
46397
|
+
function fetchPage(client2, nextLink, path, opts = {}) {
|
|
46398
|
+
return nextLink !== void 0 ? client2.getAbsolute(nextLink, opts) : client2.get(path, opts);
|
|
46399
|
+
}
|
|
46400
|
+
function plainCollection(data) {
|
|
46401
|
+
const items = Array.isArray(data?.value) ? data.value : [];
|
|
46402
|
+
const out = { count: items.length, items: items.map(stripOData) };
|
|
46403
|
+
const next = data?.["@odata.nextLink"];
|
|
46404
|
+
if (next) out.nextLink = next;
|
|
46405
|
+
return out;
|
|
46406
|
+
}
|
|
46407
|
+
|
|
46376
46408
|
// src/tools/mail.ts
|
|
46377
46409
|
var VIEWS2 = ["compact", "full", "raw"];
|
|
46378
46410
|
var WELL_KNOWN = [
|
|
@@ -46405,11 +46437,14 @@ function registerMailTools(server, client2) {
|
|
|
46405
46437
|
annotations: { readOnlyHint: true },
|
|
46406
46438
|
inputSchema: external_exports.object({
|
|
46407
46439
|
view: viewParam(VIEWS2),
|
|
46408
|
-
limit: external_exports.number().int().min(1).max(200).optional().describe("Max folders (default 50)")
|
|
46440
|
+
limit: external_exports.number().int().min(1).max(200).optional().describe("Max folders (default 50)"),
|
|
46441
|
+
nextLink: nextLinkParam
|
|
46409
46442
|
})
|
|
46410
46443
|
},
|
|
46411
|
-
async ({ view, limit }) => {
|
|
46412
|
-
const data = await
|
|
46444
|
+
async ({ view, limit, nextLink }) => {
|
|
46445
|
+
const data = await fetchPage(
|
|
46446
|
+
client2,
|
|
46447
|
+
nextLink,
|
|
46413
46448
|
`/me/mailfolders${qs({ $top: limit ?? 50 })}`
|
|
46414
46449
|
);
|
|
46415
46450
|
if (resolveView(view, VIEWS2) === "raw") return minifiedResult(data);
|
|
@@ -46419,7 +46454,7 @@ function registerMailTools(server, client2) {
|
|
|
46419
46454
|
server.registerTool(
|
|
46420
46455
|
"outlook_list_messages",
|
|
46421
46456
|
{
|
|
46422
|
-
description: "List messages in a folder, newest first. Defaults to the inbox. Use `unreadOnly` for a triage view. Bodies are NOT included \u2014 call outlook_get_message for one. Note `search` and `unreadOnly` cannot be combined (the API rejects $search with $filter); pass `search` alone to search the whole mailbox.",
|
|
46457
|
+
description: "List messages in a folder, newest first. Defaults to the inbox. Use `unreadOnly` for a triage view. Bodies are NOT included \u2014 call outlook_get_message for one. Note `search` and `unreadOnly` cannot be combined on a first-page request (the API rejects $search with $filter); pass `search` alone to search the whole mailbox. When following a `nextLink`, both are ignored, so echoing them back is harmless.",
|
|
46423
46458
|
annotations: { readOnlyHint: true },
|
|
46424
46459
|
inputSchema: external_exports.object({
|
|
46425
46460
|
view: viewParam(VIEWS2),
|
|
@@ -46427,11 +46462,12 @@ function registerMailTools(server, client2) {
|
|
|
46427
46462
|
limit: external_exports.number().int().min(1).max(100).optional().describe("Max messages (default 25)"),
|
|
46428
46463
|
skip: external_exports.number().int().min(0).optional().describe("Offset for paging"),
|
|
46429
46464
|
unreadOnly: external_exports.boolean().optional().describe("Only unread messages"),
|
|
46430
|
-
search: external_exports.string().optional().describe("Full-text search across the whole mailbox; cannot combine with unreadOnly")
|
|
46465
|
+
search: external_exports.string().optional().describe("Full-text search across the whole mailbox; cannot combine with unreadOnly on a first page (ignored with nextLink)"),
|
|
46466
|
+
nextLink: nextLinkParam
|
|
46431
46467
|
})
|
|
46432
46468
|
},
|
|
46433
|
-
async ({ view, folder, limit, skip, unreadOnly, search }) => {
|
|
46434
|
-
if (search !== void 0 && unreadOnly === true) {
|
|
46469
|
+
async ({ view, folder, limit, skip, unreadOnly, search, nextLink }) => {
|
|
46470
|
+
if (nextLink === void 0 && search !== void 0 && unreadOnly === true) {
|
|
46435
46471
|
throw new McpToolError("`search` and `unreadOnly` cannot be combined.", {
|
|
46436
46472
|
hint: "Outlook rejects $search together with $filter. Search first, then filter the results."
|
|
46437
46473
|
});
|
|
@@ -46448,7 +46484,11 @@ function registerMailTools(server, client2) {
|
|
|
46448
46484
|
params.$orderby = "ReceivedDateTime desc";
|
|
46449
46485
|
if (unreadOnly === true) params.$filter = "IsRead eq false";
|
|
46450
46486
|
}
|
|
46451
|
-
const data = await
|
|
46487
|
+
const data = await fetchPage(
|
|
46488
|
+
client2,
|
|
46489
|
+
nextLink,
|
|
46490
|
+
`${base}${qs(params)}`
|
|
46491
|
+
);
|
|
46452
46492
|
const v = resolveView(view, VIEWS2);
|
|
46453
46493
|
if (v === "raw") return minifiedResult(data);
|
|
46454
46494
|
return minifiedResult(
|
|
@@ -46482,19 +46522,19 @@ function registerMailTools(server, client2) {
|
|
|
46482
46522
|
description: "List a message's attachments (name, size, content type). Metadata only \u2014 this deliberately does not download bytes.",
|
|
46483
46523
|
annotations: { readOnlyHint: true },
|
|
46484
46524
|
inputSchema: external_exports.object({
|
|
46485
|
-
id: external_exports.string().min(1).describe("Message Id")
|
|
46525
|
+
id: external_exports.string().min(1).describe("Message Id"),
|
|
46526
|
+
nextLink: nextLinkParam
|
|
46486
46527
|
})
|
|
46487
46528
|
},
|
|
46488
|
-
async ({ id }) => {
|
|
46489
|
-
const data = await
|
|
46529
|
+
async ({ id, nextLink }) => {
|
|
46530
|
+
const data = await fetchPage(
|
|
46531
|
+
client2,
|
|
46532
|
+
nextLink,
|
|
46490
46533
|
`/me/messages/${encodeURIComponent(id)}/attachments${qs({
|
|
46491
46534
|
$select: "Id,Name,Size,ContentType"
|
|
46492
46535
|
})}`
|
|
46493
46536
|
);
|
|
46494
|
-
return minifiedResult(
|
|
46495
|
-
count: data.value?.length ?? 0,
|
|
46496
|
-
items: (data.value ?? []).map(stripOData)
|
|
46497
|
-
});
|
|
46537
|
+
return minifiedResult(plainCollection(data));
|
|
46498
46538
|
}
|
|
46499
46539
|
);
|
|
46500
46540
|
}
|
|
@@ -46541,12 +46581,15 @@ function registerCalendarTools(server, client2) {
|
|
|
46541
46581
|
timeZone: external_exports.string().optional().describe(
|
|
46542
46582
|
'Windows time-zone name for the returned times, e.g. "Eastern Standard Time". NOT an IANA name like America/New_York.'
|
|
46543
46583
|
),
|
|
46544
|
-
limit: external_exports.number().int().min(1).max(200).optional().describe("Max events (default 50)")
|
|
46584
|
+
limit: external_exports.number().int().min(1).max(200).optional().describe("Max events (default 50)"),
|
|
46585
|
+
nextLink: nextLinkParam
|
|
46545
46586
|
})
|
|
46546
46587
|
},
|
|
46547
|
-
async ({ view, start, end, timeZone, limit }) => {
|
|
46588
|
+
async ({ view, start, end, timeZone, limit, nextLink }) => {
|
|
46548
46589
|
const zone = timeZone ?? await mailboxTimeZone(client2);
|
|
46549
|
-
const data = await
|
|
46590
|
+
const data = await fetchPage(
|
|
46591
|
+
client2,
|
|
46592
|
+
nextLink,
|
|
46550
46593
|
`/me/calendarview${qs2({
|
|
46551
46594
|
startDateTime: start,
|
|
46552
46595
|
endDateTime: end,
|
|
@@ -46591,18 +46634,14 @@ function registerCalendarTools(server, client2) {
|
|
|
46591
46634
|
description: "List the calendars in the mailbox, for picking one to read or write against.",
|
|
46592
46635
|
annotations: { readOnlyHint: true },
|
|
46593
46636
|
inputSchema: external_exports.object({
|
|
46594
|
-
view: viewParam(VIEWS2)
|
|
46637
|
+
view: viewParam(VIEWS2),
|
|
46638
|
+
nextLink: nextLinkParam
|
|
46595
46639
|
})
|
|
46596
46640
|
},
|
|
46597
|
-
async ({ view }) => {
|
|
46598
|
-
const data = await client2
|
|
46599
|
-
`/me/calendars${qs2({ $select: "Id,Name,Color,CanEdit,Owner" })}`
|
|
46600
|
-
);
|
|
46641
|
+
async ({ view, nextLink }) => {
|
|
46642
|
+
const data = await fetchPage(client2, nextLink, `/me/calendars${qs2({ $select: "Id,Name,Color,CanEdit,Owner" })}`);
|
|
46601
46643
|
if (resolveView(view, VIEWS2) === "raw") return minifiedResult(data);
|
|
46602
|
-
return minifiedResult(
|
|
46603
|
-
count: data.value?.length ?? 0,
|
|
46604
|
-
items: (data.value ?? []).map(stripOData)
|
|
46605
|
-
});
|
|
46644
|
+
return minifiedResult(plainCollection(data));
|
|
46606
46645
|
}
|
|
46607
46646
|
);
|
|
46608
46647
|
}
|
|
@@ -46620,7 +46659,7 @@ function qs3(params) {
|
|
|
46620
46659
|
}
|
|
46621
46660
|
function collection(data, raw) {
|
|
46622
46661
|
if (raw) return data;
|
|
46623
|
-
return
|
|
46662
|
+
return plainCollection(data);
|
|
46624
46663
|
}
|
|
46625
46664
|
function registerDirectoryTools(server, client2) {
|
|
46626
46665
|
server.registerTool(
|
|
@@ -46649,11 +46688,14 @@ function registerDirectoryTools(server, client2) {
|
|
|
46649
46688
|
inputSchema: external_exports.object({
|
|
46650
46689
|
view: viewParam(VIEWS2),
|
|
46651
46690
|
limit: external_exports.number().int().min(1).max(200).optional().describe("Max contacts (default 50)"),
|
|
46652
|
-
skip: external_exports.number().int().min(0).optional().describe("Offset for paging")
|
|
46691
|
+
skip: external_exports.number().int().min(0).optional().describe("Offset for paging"),
|
|
46692
|
+
nextLink: nextLinkParam
|
|
46653
46693
|
})
|
|
46654
46694
|
},
|
|
46655
|
-
async ({ view, limit, skip }) => {
|
|
46656
|
-
const data = await
|
|
46695
|
+
async ({ view, limit, skip, nextLink }) => {
|
|
46696
|
+
const data = await fetchPage(
|
|
46697
|
+
client2,
|
|
46698
|
+
nextLink,
|
|
46657
46699
|
`/me/contacts${qs3({
|
|
46658
46700
|
$top: limit ?? 50,
|
|
46659
46701
|
$skip: skip,
|
|
@@ -46672,11 +46714,14 @@ function registerDirectoryTools(server, client2) {
|
|
|
46672
46714
|
annotations: { readOnlyHint: true },
|
|
46673
46715
|
inputSchema: external_exports.object({
|
|
46674
46716
|
view: viewParam(VIEWS2),
|
|
46675
|
-
limit: external_exports.number().int().min(1).max(100).optional().describe("Max people (default 25)")
|
|
46717
|
+
limit: external_exports.number().int().min(1).max(100).optional().describe("Max people (default 25)"),
|
|
46718
|
+
nextLink: nextLinkParam
|
|
46676
46719
|
})
|
|
46677
46720
|
},
|
|
46678
|
-
async ({ view, limit }) => {
|
|
46679
|
-
const data = await
|
|
46721
|
+
async ({ view, limit, nextLink }) => {
|
|
46722
|
+
const data = await fetchPage(
|
|
46723
|
+
client2,
|
|
46724
|
+
nextLink,
|
|
46680
46725
|
`/me/people${qs3({
|
|
46681
46726
|
$top: limit ?? 25,
|
|
46682
46727
|
$select: "Id,DisplayName,ScoredEmailAddresses,JobTitle,CompanyName"
|
|
@@ -46692,11 +46737,14 @@ function registerDirectoryTools(server, client2) {
|
|
|
46692
46737
|
annotations: { readOnlyHint: true },
|
|
46693
46738
|
inputSchema: external_exports.object({
|
|
46694
46739
|
view: viewParam(VIEWS2),
|
|
46695
|
-
limit: external_exports.number().int().min(1).max(200).optional().describe("Max tasks (default 50)")
|
|
46740
|
+
limit: external_exports.number().int().min(1).max(200).optional().describe("Max tasks (default 50)"),
|
|
46741
|
+
nextLink: nextLinkParam
|
|
46696
46742
|
})
|
|
46697
46743
|
},
|
|
46698
|
-
async ({ view, limit }) => {
|
|
46699
|
-
const data = await
|
|
46744
|
+
async ({ view, limit, nextLink }) => {
|
|
46745
|
+
const data = await fetchPage(
|
|
46746
|
+
client2,
|
|
46747
|
+
nextLink,
|
|
46700
46748
|
`/me/tasks${qs3({
|
|
46701
46749
|
$top: limit ?? 50,
|
|
46702
46750
|
$select: "Id,Subject,Status,Importance,DueDateTime,CompletedDateTime"
|
package/dist/client.js
CHANGED
|
@@ -198,17 +198,35 @@ export class OutlookClient {
|
|
|
198
198
|
headers: prefer.length ? { Prefer: prefer.join(', ') } : undefined,
|
|
199
199
|
});
|
|
200
200
|
}
|
|
201
|
-
/**
|
|
202
|
-
|
|
201
|
+
/**
|
|
202
|
+
* GET an absolute URL — used only to follow `@odata.nextLink`.
|
|
203
|
+
*
|
|
204
|
+
* The link is absolute and already carries the base path
|
|
205
|
+
* (`https://outlook.office.com/api/v2.0/me/messages?$skip=25`), while the
|
|
206
|
+
* underlying client appends every path to that same base. So the base path is
|
|
207
|
+
* stripped here, and a link that is not under it — another origin, or another
|
|
208
|
+
* path on the same host — is refused rather than sent the bearer token.
|
|
209
|
+
*/
|
|
210
|
+
async getAbsolute(url, opts = {}) {
|
|
203
211
|
this.#requireConfigured();
|
|
204
212
|
const base = new URL(this.#baseUrl);
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
213
|
+
let target;
|
|
214
|
+
try {
|
|
215
|
+
target = new URL(url);
|
|
216
|
+
}
|
|
217
|
+
catch {
|
|
218
|
+
throw new McpToolError(`Refusing to follow a link that is not a URL: ${url}`, {
|
|
219
|
+
hint: 'Pass the nextLink value exactly as a previous listing returned it.',
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
const prefix = base.pathname.replace(/\/+$/, '');
|
|
223
|
+
if (target.origin !== base.origin ||
|
|
224
|
+
!(target.pathname === prefix || target.pathname.startsWith(`${prefix}/`))) {
|
|
225
|
+
throw new McpToolError(`Refusing to follow a link to ${target.origin}${target.pathname}.`, {
|
|
226
|
+
hint: `Pagination links must stay under the Outlook API base (${this.#baseUrl}).`,
|
|
209
227
|
});
|
|
210
228
|
}
|
|
211
|
-
return this
|
|
229
|
+
return this.get(target.pathname.slice(prefix.length) + target.search, opts);
|
|
212
230
|
}
|
|
213
231
|
/**
|
|
214
232
|
* The single write path. Everything mutating goes through here so auth and
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { stripOData } from '../view.js';
|
|
3
|
+
/**
|
|
4
|
+
* Paging for every list tool.
|
|
5
|
+
*
|
|
6
|
+
* Outlook caps each page (`$top`) and signals the rest with
|
|
7
|
+
* `@odata.nextLink`. A listing that drops that link is indistinguishable from a
|
|
8
|
+
* complete one, so the model says with confidence that an item does not
|
|
9
|
+
* exist. Every list tool therefore returns `nextLink` when there is more, and
|
|
10
|
+
* accepts it back to fetch the next page.
|
|
11
|
+
*/
|
|
12
|
+
export const nextLinkParam = z
|
|
13
|
+
.string()
|
|
14
|
+
.min(1)
|
|
15
|
+
.optional()
|
|
16
|
+
.describe('Continue a previous listing: pass its `nextLink` value unchanged to fetch the next page. When set, the paging/filter arguments are taken from the link and ignored here.');
|
|
17
|
+
/** Fetch the first page at `path`, or the page a previous `nextLink` points at. */
|
|
18
|
+
export function fetchPage(client, nextLink, path, opts = {}) {
|
|
19
|
+
return nextLink !== undefined ? client.getAbsolute(nextLink, opts) : client.get(path, opts);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Pass a collection through without a typed projection, trimming the OData
|
|
23
|
+
* envelope from each item but keeping the paging link.
|
|
24
|
+
*/
|
|
25
|
+
export function plainCollection(data) {
|
|
26
|
+
const items = Array.isArray(data?.value) ? data.value : [];
|
|
27
|
+
const out = { count: items.length, items: items.map(stripOData) };
|
|
28
|
+
const next = data?.['@odata.nextLink'];
|
|
29
|
+
if (next)
|
|
30
|
+
out.nextLink = next;
|
|
31
|
+
return out;
|
|
32
|
+
}
|
package/dist/tools/calendar.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { minifiedResult, resolveView, viewParam } from '@chrischall/mcp-utils';
|
|
3
|
-
import { compactEvent, fullEvent, projectCollection,
|
|
3
|
+
import { compactEvent, fullEvent, projectCollection, } from '../view.js';
|
|
4
|
+
import { fetchPage, nextLinkParam, plainCollection } from './_paging.js';
|
|
4
5
|
import { VIEWS } from './mail.js';
|
|
5
6
|
import { mailboxTimeZone } from '../timezone.js';
|
|
6
7
|
const EVENT_SELECT = 'Id,Subject,Start,End,Location,Organizer,IsAllDay,IsCancelled,ShowAs,OnlineMeetingUrl,BodyPreview';
|
|
@@ -29,10 +30,11 @@ export function registerCalendarTools(server, client) {
|
|
|
29
30
|
.optional()
|
|
30
31
|
.describe('Windows time-zone name for the returned times, e.g. "Eastern Standard Time". NOT an IANA name like America/New_York.'),
|
|
31
32
|
limit: z.number().int().min(1).max(200).optional().describe('Max events (default 50)'),
|
|
33
|
+
nextLink: nextLinkParam,
|
|
32
34
|
}),
|
|
33
|
-
}, async ({ view, start, end, timeZone, limit }) => {
|
|
35
|
+
}, async ({ view, start, end, timeZone, limit, nextLink }) => {
|
|
34
36
|
const zone = timeZone ?? (await mailboxTimeZone(client));
|
|
35
|
-
const data = await client
|
|
37
|
+
const data = await fetchPage(client, nextLink, `/me/calendarview${qs({
|
|
36
38
|
startDateTime: start,
|
|
37
39
|
endDateTime: end,
|
|
38
40
|
$select: EVENT_SELECT,
|
|
@@ -65,14 +67,12 @@ export function registerCalendarTools(server, client) {
|
|
|
65
67
|
annotations: { readOnlyHint: true },
|
|
66
68
|
inputSchema: z.object({
|
|
67
69
|
view: viewParam(VIEWS),
|
|
70
|
+
nextLink: nextLinkParam,
|
|
68
71
|
}),
|
|
69
|
-
}, async ({ view }) => {
|
|
70
|
-
const data = await client
|
|
72
|
+
}, async ({ view, nextLink }) => {
|
|
73
|
+
const data = await fetchPage(client, nextLink, `/me/calendars${qs({ $select: 'Id,Name,Color,CanEdit,Owner' })}`);
|
|
71
74
|
if (resolveView(view, VIEWS) === 'raw')
|
|
72
75
|
return minifiedResult(data);
|
|
73
|
-
return minifiedResult(
|
|
74
|
-
count: data.value?.length ?? 0,
|
|
75
|
-
items: (data.value ?? []).map(stripOData),
|
|
76
|
-
});
|
|
76
|
+
return minifiedResult(plainCollection(data));
|
|
77
77
|
});
|
|
78
78
|
}
|
package/dist/tools/directory.js
CHANGED
|
@@ -2,6 +2,7 @@ import { z } from 'zod';
|
|
|
2
2
|
import { minifiedResult, resolveView, viewParam } from '@chrischall/mcp-utils';
|
|
3
3
|
import { stripOData } from '../view.js';
|
|
4
4
|
import { VIEWS } from './mail.js';
|
|
5
|
+
import { fetchPage, nextLinkParam, plainCollection } from './_paging.js';
|
|
5
6
|
function qs(params) {
|
|
6
7
|
const parts = [];
|
|
7
8
|
for (const [k, v] of Object.entries(params)) {
|
|
@@ -14,7 +15,7 @@ function qs(params) {
|
|
|
14
15
|
function collection(data, raw) {
|
|
15
16
|
if (raw)
|
|
16
17
|
return data;
|
|
17
|
-
return
|
|
18
|
+
return plainCollection(data);
|
|
18
19
|
}
|
|
19
20
|
export function registerDirectoryTools(server, client) {
|
|
20
21
|
server.registerTool('outlook_get_profile', {
|
|
@@ -34,9 +35,10 @@ export function registerDirectoryTools(server, client) {
|
|
|
34
35
|
view: viewParam(VIEWS),
|
|
35
36
|
limit: z.number().int().min(1).max(200).optional().describe('Max contacts (default 50)'),
|
|
36
37
|
skip: z.number().int().min(0).optional().describe('Offset for paging'),
|
|
38
|
+
nextLink: nextLinkParam,
|
|
37
39
|
}),
|
|
38
|
-
}, async ({ view, limit, skip }) => {
|
|
39
|
-
const data = await client
|
|
40
|
+
}, async ({ view, limit, skip, nextLink }) => {
|
|
41
|
+
const data = await fetchPage(client, nextLink, `/me/contacts${qs({
|
|
40
42
|
$top: limit ?? 50,
|
|
41
43
|
$skip: skip,
|
|
42
44
|
// `MobilePhone1`, not `MobilePhone`: the latter is the Graph name and
|
|
@@ -51,9 +53,10 @@ export function registerDirectoryTools(server, client) {
|
|
|
51
53
|
inputSchema: z.object({
|
|
52
54
|
view: viewParam(VIEWS),
|
|
53
55
|
limit: z.number().int().min(1).max(100).optional().describe('Max people (default 25)'),
|
|
56
|
+
nextLink: nextLinkParam,
|
|
54
57
|
}),
|
|
55
|
-
}, async ({ view, limit }) => {
|
|
56
|
-
const data = await client
|
|
58
|
+
}, async ({ view, limit, nextLink }) => {
|
|
59
|
+
const data = await fetchPage(client, nextLink, `/me/people${qs({
|
|
57
60
|
$top: limit ?? 25,
|
|
58
61
|
$select: 'Id,DisplayName,ScoredEmailAddresses,JobTitle,CompanyName',
|
|
59
62
|
})}`);
|
|
@@ -65,9 +68,10 @@ export function registerDirectoryTools(server, client) {
|
|
|
65
68
|
inputSchema: z.object({
|
|
66
69
|
view: viewParam(VIEWS),
|
|
67
70
|
limit: z.number().int().min(1).max(200).optional().describe('Max tasks (default 50)'),
|
|
71
|
+
nextLink: nextLinkParam,
|
|
68
72
|
}),
|
|
69
|
-
}, async ({ view, limit }) => {
|
|
70
|
-
const data = await client
|
|
73
|
+
}, async ({ view, limit, nextLink }) => {
|
|
74
|
+
const data = await fetchPage(client, nextLink, `/me/tasks${qs({
|
|
71
75
|
$top: limit ?? 50,
|
|
72
76
|
$select: 'Id,Subject,Status,Importance,DueDateTime,CompletedDateTime',
|
|
73
77
|
})}`);
|
package/dist/tools/mail.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { minifiedResult, resolveView, viewParam, McpToolError } from '@chrischall/mcp-utils';
|
|
3
|
-
import { compactFolder, compactMessage, fullMessage, projectCollection,
|
|
3
|
+
import { compactFolder, compactMessage, fullMessage, projectCollection, } from '../view.js';
|
|
4
|
+
import { fetchPage, nextLinkParam, plainCollection } from './_paging.js';
|
|
4
5
|
export const VIEWS = ['compact', 'full', 'raw'];
|
|
5
6
|
/**
|
|
6
7
|
* Well-known folder names the API accepts wherever a folder id is taken.
|
|
@@ -40,15 +41,16 @@ export function registerMailTools(server, client) {
|
|
|
40
41
|
inputSchema: z.object({
|
|
41
42
|
view: viewParam(VIEWS),
|
|
42
43
|
limit: z.number().int().min(1).max(200).optional().describe('Max folders (default 50)'),
|
|
44
|
+
nextLink: nextLinkParam,
|
|
43
45
|
}),
|
|
44
|
-
}, async ({ view, limit }) => {
|
|
45
|
-
const data = await client
|
|
46
|
+
}, async ({ view, limit, nextLink }) => {
|
|
47
|
+
const data = await fetchPage(client, nextLink, `/me/mailfolders${qs({ $top: limit ?? 50 })}`);
|
|
46
48
|
if (resolveView(view, VIEWS) === 'raw')
|
|
47
49
|
return minifiedResult(data);
|
|
48
50
|
return minifiedResult(projectCollection(data, compactFolder, 'mail folder'));
|
|
49
51
|
});
|
|
50
52
|
server.registerTool('outlook_list_messages', {
|
|
51
|
-
description: "List messages in a folder, newest first. Defaults to the inbox. Use `unreadOnly` for a triage view. Bodies are NOT included — call outlook_get_message for one. Note `search` and `unreadOnly` cannot be combined (the API rejects $search with $filter); pass `search` alone to search the whole mailbox.",
|
|
53
|
+
description: "List messages in a folder, newest first. Defaults to the inbox. Use `unreadOnly` for a triage view. Bodies are NOT included — call outlook_get_message for one. Note `search` and `unreadOnly` cannot be combined on a first-page request (the API rejects $search with $filter); pass `search` alone to search the whole mailbox. When following a `nextLink`, both are ignored, so echoing them back is harmless.",
|
|
52
54
|
annotations: { readOnlyHint: true },
|
|
53
55
|
inputSchema: z.object({
|
|
54
56
|
view: viewParam(VIEWS),
|
|
@@ -62,10 +64,13 @@ export function registerMailTools(server, client) {
|
|
|
62
64
|
search: z
|
|
63
65
|
.string()
|
|
64
66
|
.optional()
|
|
65
|
-
.describe('Full-text search across the whole mailbox; cannot combine with unreadOnly'),
|
|
67
|
+
.describe('Full-text search across the whole mailbox; cannot combine with unreadOnly on a first page (ignored with nextLink)'),
|
|
68
|
+
nextLink: nextLinkParam,
|
|
66
69
|
}),
|
|
67
|
-
}, async ({ view, folder, limit, skip, unreadOnly, search }) => {
|
|
68
|
-
|
|
70
|
+
}, async ({ view, folder, limit, skip, unreadOnly, search, nextLink }) => {
|
|
71
|
+
// With nextLink the filters come from the link and are ignored here, so
|
|
72
|
+
// the conflict only matters for a first-page request.
|
|
73
|
+
if (nextLink === undefined && search !== undefined && unreadOnly === true) {
|
|
69
74
|
throw new McpToolError('`search` and `unreadOnly` cannot be combined.', {
|
|
70
75
|
hint: 'Outlook rejects $search together with $filter. Search first, then filter the results.',
|
|
71
76
|
});
|
|
@@ -86,7 +91,7 @@ export function registerMailTools(server, client) {
|
|
|
86
91
|
if (unreadOnly === true)
|
|
87
92
|
params.$filter = 'IsRead eq false';
|
|
88
93
|
}
|
|
89
|
-
const data = await client
|
|
94
|
+
const data = await fetchPage(client, nextLink, `${base}${qs(params)}`);
|
|
90
95
|
const v = resolveView(view, VIEWS);
|
|
91
96
|
if (v === 'raw')
|
|
92
97
|
return minifiedResult(data);
|
|
@@ -117,14 +122,12 @@ export function registerMailTools(server, client) {
|
|
|
117
122
|
annotations: { readOnlyHint: true },
|
|
118
123
|
inputSchema: z.object({
|
|
119
124
|
id: z.string().min(1).describe('Message Id'),
|
|
125
|
+
nextLink: nextLinkParam,
|
|
120
126
|
}),
|
|
121
|
-
}, async ({ id }) => {
|
|
122
|
-
const data = await client
|
|
127
|
+
}, async ({ id, nextLink }) => {
|
|
128
|
+
const data = await fetchPage(client, nextLink, `/me/messages/${encodeURIComponent(id)}/attachments${qs({
|
|
123
129
|
$select: 'Id,Name,Size,ContentType',
|
|
124
130
|
})}`);
|
|
125
|
-
return minifiedResult(
|
|
126
|
-
count: data.value?.length ?? 0,
|
|
127
|
-
items: (data.value ?? []).map(stripOData),
|
|
128
|
-
});
|
|
131
|
+
return minifiedResult(plainCollection(data));
|
|
129
132
|
});
|
|
130
133
|
}
|
package/dist/version.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chrischall/office-outlook-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"mcpName": "io.github.chrischall/office-outlook-mcp",
|
|
5
5
|
"description": "Outlook / Microsoft 365 MCP server for Claude — developed and maintained by AI (Claude Code)",
|
|
6
6
|
"author": "Claude Code (AI) <https://www.anthropic.com/claude>",
|
package/server.json
CHANGED
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
"url": "https://github.com/chrischall/office-outlook-mcp",
|
|
7
7
|
"source": "github"
|
|
8
8
|
},
|
|
9
|
-
"version": "0.
|
|
9
|
+
"version": "0.2.0",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "@chrischall/office-outlook-mcp",
|
|
14
|
-
"version": "0.
|
|
14
|
+
"version": "0.2.0",
|
|
15
15
|
"transport": {
|
|
16
16
|
"type": "stdio"
|
|
17
17
|
},
|