@chrischall/office-outlook-mcp 0.8.2 → 0.8.3
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 +31 -24
- package/dist/tools/_paging.js +19 -5
- package/dist/tools/export.js +29 -11
- 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.8.
|
|
10
|
+
"version": "0.8.3"
|
|
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 — triage unread mail, reply, answer meeting invites and schedule meetings, plus folders, calendar, contacts and tasks, with confirmation before anything is sent",
|
|
18
|
-
"version": "0.8.
|
|
18
|
+
"version": "0.8.3",
|
|
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.8.
|
|
4
|
+
"version": "0.8.3",
|
|
5
5
|
"description": "Outlook / Microsoft 365 for Claude — triage unread mail, reply, answer meeting invites and schedule meetings, plus folders, calendar, contacts and tasks, with confirmation before anything is sent, using your signed-in browser session",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Chris Hall",
|
package/dist/bundle.js
CHANGED
|
@@ -39086,7 +39086,7 @@ var init_version = __esm({
|
|
|
39086
39086
|
"src/version.ts"() {
|
|
39087
39087
|
"use strict";
|
|
39088
39088
|
PACKAGE_NAME = "office-outlook-mcp";
|
|
39089
|
-
VERSION = "0.8.
|
|
39089
|
+
VERSION = "0.8.3";
|
|
39090
39090
|
}
|
|
39091
39091
|
});
|
|
39092
39092
|
|
|
@@ -43398,7 +43398,7 @@ var require_websocket = __commonJS({
|
|
|
43398
43398
|
var http = __require("http");
|
|
43399
43399
|
var net = __require("net");
|
|
43400
43400
|
var tls = __require("tls");
|
|
43401
|
-
var { randomBytes: randomBytes8, createHash:
|
|
43401
|
+
var { randomBytes: randomBytes8, createHash: createHash6 } = __require("crypto");
|
|
43402
43402
|
var { Duplex, Readable } = __require("stream");
|
|
43403
43403
|
var { URL: URL2 } = __require("url");
|
|
43404
43404
|
var PerMessageDeflate2 = require_permessage_deflate();
|
|
@@ -44079,7 +44079,7 @@ var require_websocket = __commonJS({
|
|
|
44079
44079
|
abortHandshake(websocket, socket, "Invalid Upgrade header");
|
|
44080
44080
|
return;
|
|
44081
44081
|
}
|
|
44082
|
-
const digest =
|
|
44082
|
+
const digest = createHash6("sha1").update(key + GUID).digest("base64");
|
|
44083
44083
|
if (res.headers["sec-websocket-accept"] !== digest) {
|
|
44084
44084
|
abortHandshake(websocket, socket, "Invalid Sec-WebSocket-Accept header");
|
|
44085
44085
|
return;
|
|
@@ -44448,7 +44448,7 @@ var require_websocket_server = __commonJS({
|
|
|
44448
44448
|
var EventEmitter = __require("events");
|
|
44449
44449
|
var http = __require("http");
|
|
44450
44450
|
var { Duplex } = __require("stream");
|
|
44451
|
-
var { createHash:
|
|
44451
|
+
var { createHash: createHash6 } = __require("crypto");
|
|
44452
44452
|
var extension2 = require_extension();
|
|
44453
44453
|
var PerMessageDeflate2 = require_permessage_deflate();
|
|
44454
44454
|
var subprotocol2 = require_subprotocol();
|
|
@@ -44755,7 +44755,7 @@ var require_websocket_server = __commonJS({
|
|
|
44755
44755
|
);
|
|
44756
44756
|
}
|
|
44757
44757
|
if (this._state > RUNNING) return abortHandshake(socket, 503);
|
|
44758
|
-
const digest =
|
|
44758
|
+
const digest = createHash6("sha1").update(key + GUID).digest("base64");
|
|
44759
44759
|
const headers = [
|
|
44760
44760
|
"HTTP/1.1 101 Switching Protocols",
|
|
44761
44761
|
"Upgrade: websocket",
|
|
@@ -50603,6 +50603,7 @@ function isMeetingMessage(m) {
|
|
|
50603
50603
|
// src/tools/_paging.ts
|
|
50604
50604
|
init_zod();
|
|
50605
50605
|
init_dist2();
|
|
50606
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
50606
50607
|
var nextLinkParam = external_exports.string().min(1).optional().describe(
|
|
50607
50608
|
"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."
|
|
50608
50609
|
);
|
|
@@ -50618,6 +50619,10 @@ function plainCollection(data) {
|
|
|
50618
50619
|
if (next) out.nextLink = next;
|
|
50619
50620
|
return out;
|
|
50620
50621
|
}
|
|
50622
|
+
var MAX_KEY_LENGTH = 16;
|
|
50623
|
+
function idKey(id) {
|
|
50624
|
+
return id.length <= MAX_KEY_LENGTH ? id : createHash4("sha256").update(id).digest("base64url").slice(0, 12);
|
|
50625
|
+
}
|
|
50621
50626
|
var fromParam = external_exports.string().min(1).optional().describe(
|
|
50622
50627
|
'Page oldest first from this point: an ISO 8601 UTC time (e.g. "2026-10-01T00:00:00Z"; one with no zone is read as UTC), or a previous result\'s `next_from` passed unchanged. The result always carries `next_from` and `has_more`.'
|
|
50623
50628
|
);
|
|
@@ -50663,24 +50668,24 @@ function decodeFrom(from) {
|
|
|
50663
50668
|
if (ms === void 0 || !Array.isArray(p?.ids) || !p.ids.every((id) => typeof id === "string")) {
|
|
50664
50669
|
throw new McpToolError("`from` is neither an ISO 8601 time nor a next_from token.", { hint: FROM_HINT });
|
|
50665
50670
|
}
|
|
50666
|
-
return { t: utcLiteral(ms), ids: p.ids };
|
|
50671
|
+
return { t: utcLiteral(ms), ids: [...new Set(p.ids.map(idKey))] };
|
|
50667
50672
|
}
|
|
50668
50673
|
function fromQuery(pos, limit) {
|
|
50669
50674
|
return { bound: `ReceivedDateTime ge ${pos.t}`, top: Math.min(limit + pos.ids.length, MAX_TOP) };
|
|
50670
50675
|
}
|
|
50671
50676
|
function fromPage(rows, pos, limit, apiHasMore) {
|
|
50672
|
-
const seen = new Set(pos.ids);
|
|
50673
|
-
const fresh = rows.filter((r) => r.Id === void 0 || !seen.has(r.Id));
|
|
50677
|
+
const seen = new Set(pos.ids.map(idKey));
|
|
50678
|
+
const fresh = rows.filter((r) => r.Id === void 0 || !seen.has(idKey(r.Id)));
|
|
50674
50679
|
const kept = fresh.slice(0, limit);
|
|
50675
50680
|
const next = nextPosition(kept, pos);
|
|
50676
50681
|
return { rows: kept, next_from: encodeFrom(next), has_more: fresh.length > limit || apiHasMore };
|
|
50677
50682
|
}
|
|
50678
50683
|
function nextPosition(rows, pos) {
|
|
50679
|
-
const dated = rows.map((r) => ({ id: r.Id, ms: r.ReceivedDateTime ? Date.parse(r.ReceivedDateTime) : NaN })).filter((r) => r.id !== void 0 && !Number.isNaN(r.ms));
|
|
50684
|
+
const dated = rows.map((r) => ({ id: r.Id === void 0 ? void 0 : idKey(r.Id), ms: r.ReceivedDateTime ? Date.parse(r.ReceivedDateTime) : NaN })).filter((r) => r.id !== void 0 && !Number.isNaN(r.ms));
|
|
50680
50685
|
if (dated.length === 0) return pos;
|
|
50681
50686
|
const newest = Math.max(...dated.map((r) => r.ms));
|
|
50682
50687
|
const tied = dated.filter((r) => r.ms === newest).map((r) => r.id);
|
|
50683
|
-
const carried = newest === Date.parse(pos.t) ? pos.ids.filter((id) => !tied.includes(id)) : [];
|
|
50688
|
+
const carried = newest === Date.parse(pos.t) ? pos.ids.map(idKey).filter((id) => !tied.includes(id)) : [];
|
|
50684
50689
|
return { t: utcLiteral(newest), ids: [...carried, ...tied] };
|
|
50685
50690
|
}
|
|
50686
50691
|
|
|
@@ -52381,7 +52386,7 @@ function registerHealthcheckTool(server, client2) {
|
|
|
52381
52386
|
// src/tools/export.ts
|
|
52382
52387
|
init_zod();
|
|
52383
52388
|
init_dist2();
|
|
52384
|
-
import { createHash as
|
|
52389
|
+
import { createHash as createHash5 } from "node:crypto";
|
|
52385
52390
|
import { readdirSync as readdirSync2, readFileSync as readFileSync4, rmdirSync, unlinkSync as unlinkSync5 } from "node:fs";
|
|
52386
52391
|
import { join as join11 } from "node:path";
|
|
52387
52392
|
|
|
@@ -52391,7 +52396,7 @@ init_errors3();
|
|
|
52391
52396
|
import { createHmac as createHmac4 } from "node:crypto";
|
|
52392
52397
|
var BLOB_DEFAULT_TTL_MS = 60 * 60 * 1e3;
|
|
52393
52398
|
var BLOB_MAX_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
52394
|
-
var
|
|
52399
|
+
var MAX_KEY_LENGTH2 = 1024;
|
|
52395
52400
|
var SAFE_SEGMENT = /^[A-Za-z0-9._~-]+$/;
|
|
52396
52401
|
function configError(reason) {
|
|
52397
52402
|
return new McpToolError(`MCP_BLOB_BASE_URL is not usable: ${reason}.`, {
|
|
@@ -52424,8 +52429,8 @@ function badInput(message) {
|
|
|
52424
52429
|
function encodeKey(key) {
|
|
52425
52430
|
if (typeof key !== "string" || key.length === 0)
|
|
52426
52431
|
throw badInput("Blob key must be a non-empty string.");
|
|
52427
|
-
if (key.length >
|
|
52428
|
-
throw badInput(`Blob key is longer than ${
|
|
52432
|
+
if (key.length > MAX_KEY_LENGTH2)
|
|
52433
|
+
throw badInput(`Blob key is longer than ${MAX_KEY_LENGTH2} characters.`);
|
|
52429
52434
|
const segments = key.split("/");
|
|
52430
52435
|
for (const seg of segments) {
|
|
52431
52436
|
if (seg === "" || seg === "." || seg === ".." || !SAFE_SEGMENT.test(seg)) {
|
|
@@ -53082,6 +53087,7 @@ var eventArgsSchema = external_exports.object({
|
|
|
53082
53087
|
attachments: external_exports.boolean().optional()
|
|
53083
53088
|
});
|
|
53084
53089
|
var positionSchema = external_exports.union([
|
|
53090
|
+
external_exports.object({ at: external_exports.object({ t: external_exports.string().min(1), ids: external_exports.array(external_exports.string()) }) }),
|
|
53085
53091
|
external_exports.object({ from: external_exports.string().min(1) }),
|
|
53086
53092
|
external_exports.object({ link: external_exports.string().min(1).optional(), offset: external_exports.number().int().min(0).max(MAX_TOP2) })
|
|
53087
53093
|
]);
|
|
@@ -53292,7 +53298,7 @@ async function linkBatch(pos, limit, first, follow, keep = () => true) {
|
|
|
53292
53298
|
return end;
|
|
53293
53299
|
}
|
|
53294
53300
|
function shortId(id) {
|
|
53295
|
-
return
|
|
53301
|
+
return createHash5("sha256").update(id).digest("hex").slice(0, 16);
|
|
53296
53302
|
}
|
|
53297
53303
|
function uniqueName(name, used) {
|
|
53298
53304
|
let candidate = name;
|
|
@@ -53683,11 +53689,11 @@ function registerExportTools(server, client2, opts = {}) {
|
|
|
53683
53689
|
const folder = args.search !== void 0 ? "all" : args.folder ?? "inbox";
|
|
53684
53690
|
const path = folder === "all" ? "/me/messages" : `/me/mailfolders/${encodeURIComponent(folder)}/messages`;
|
|
53685
53691
|
const perPage = args.attachments === true ? Math.min(pageSize, ATTACHMENT_PAGE_SIZE) : pageSize;
|
|
53686
|
-
const
|
|
53687
|
-
|
|
53692
|
+
const stored = resumed?.pos;
|
|
53693
|
+
const pos = stored === void 0 ? args.search !== void 0 ? { offset: 0 } : { at: { t: since ?? EPOCH, ids: [] } } : "from" in stored ? { at: decodeFrom(stored.from) } : "at" in stored ? { at: decodeFrom(encodeFrom(stored.at)) } : stored;
|
|
53688
53694
|
const fetch2 = async (p, remaining) => {
|
|
53689
|
-
if ("
|
|
53690
|
-
const at =
|
|
53695
|
+
if ("at" in p) {
|
|
53696
|
+
const { at } = p;
|
|
53691
53697
|
const limit = Math.min(remaining, perPage);
|
|
53692
53698
|
const { bound, top } = fromQuery(at, limit);
|
|
53693
53699
|
const filter = until ? `${bound} and ReceivedDateTime lt ${until}` : bound;
|
|
@@ -53704,7 +53710,7 @@ function registerExportTools(server, client2, opts = {}) {
|
|
|
53704
53710
|
posAfter: () => p
|
|
53705
53711
|
};
|
|
53706
53712
|
}
|
|
53707
|
-
if (page.has_more && page.next_from ===
|
|
53713
|
+
if (page.has_more && page.next_from === encodeFrom(at)) {
|
|
53708
53714
|
return {
|
|
53709
53715
|
rows: page.rows,
|
|
53710
53716
|
pos: p,
|
|
@@ -53715,10 +53721,10 @@ function registerExportTools(server, client2, opts = {}) {
|
|
|
53715
53721
|
}
|
|
53716
53722
|
return {
|
|
53717
53723
|
rows: page.rows,
|
|
53718
|
-
pos: {
|
|
53724
|
+
pos: { at: decodeFrom(page.next_from) },
|
|
53719
53725
|
hasMore: page.has_more,
|
|
53720
53726
|
// The first n rows are fresh and ascending, so the keyset position after them is computed the same way.
|
|
53721
|
-
posAfter: (n) => n >= page.rows.length ? {
|
|
53727
|
+
posAfter: (n) => n >= page.rows.length ? { at: decodeFrom(page.next_from) } : { at: decodeFrom(fromPage(page.rows.slice(0, n), at, n, true).next_from) }
|
|
53722
53728
|
};
|
|
53723
53729
|
}
|
|
53724
53730
|
return linkBatch(
|
|
@@ -53806,8 +53812,9 @@ function registerExportTools(server, client2, opts = {}) {
|
|
|
53806
53812
|
}
|
|
53807
53813
|
const prefer = args.time_zone ? `outlook.timezone="${args.time_zone}"` : void 0;
|
|
53808
53814
|
const path = args.calendar !== void 0 ? `/me/calendars/${encodeURIComponent(args.calendar)}/calendarview` : "/me/calendarview";
|
|
53809
|
-
const
|
|
53810
|
-
if ("
|
|
53815
|
+
const stored = resumed?.pos ?? { offset: 0 };
|
|
53816
|
+
if (!("offset" in stored)) throw new McpToolError("`cursor` is not a valid export cursor for this tool.");
|
|
53817
|
+
const pos = stored;
|
|
53811
53818
|
const fetch2 = (p, remaining) => linkBatch(
|
|
53812
53819
|
p,
|
|
53813
53820
|
remaining,
|
package/dist/tools/_paging.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
1
2
|
import { z } from 'zod';
|
|
2
3
|
import { McpToolError } from '@chrischall/mcp-utils';
|
|
3
4
|
import { stripOData } from '../view.js';
|
|
@@ -38,6 +39,19 @@ export function plainCollection(data) {
|
|
|
38
39
|
out.nextLink = next;
|
|
39
40
|
return out;
|
|
40
41
|
}
|
|
42
|
+
/** An Id this short is kept as is (a key, or a test fixture); a real Outlook Id is ~150 characters. */
|
|
43
|
+
const MAX_KEY_LENGTH = 16;
|
|
44
|
+
/**
|
|
45
|
+
* The key a position keeps for a message Id: the Id itself when short, else
|
|
46
|
+
* 12 base64url characters (72 bits) of its SHA-256. Keys only tell apart the
|
|
47
|
+
* messages tied at ONE instant, so a collision is not a practical concern,
|
|
48
|
+
* and a burst of messages in one second costs ~15 characters each in the token
|
|
49
|
+
* rather than a whole Id. A token from an earlier release, carrying whole Ids,
|
|
50
|
+
* is re-keyed on read.
|
|
51
|
+
*/
|
|
52
|
+
export function idKey(id) {
|
|
53
|
+
return id.length <= MAX_KEY_LENGTH ? id : createHash('sha256').update(id).digest('base64url').slice(0, 12);
|
|
54
|
+
}
|
|
41
55
|
export const fromParam = z
|
|
42
56
|
.string()
|
|
43
57
|
.min(1)
|
|
@@ -104,7 +118,7 @@ export function decodeFrom(from) {
|
|
|
104
118
|
!p.ids.every((id) => typeof id === 'string')) {
|
|
105
119
|
throw new McpToolError('`from` is neither an ISO 8601 time nor a next_from token.', { hint: FROM_HINT });
|
|
106
120
|
}
|
|
107
|
-
return { t: utcLiteral(ms), ids: p.ids };
|
|
121
|
+
return { t: utcLiteral(ms), ids: [...new Set(p.ids.map(idKey))] };
|
|
108
122
|
}
|
|
109
123
|
/**
|
|
110
124
|
* The `$filter` date bound and `$top` for a page from `pos`. The bound is
|
|
@@ -121,21 +135,21 @@ export function fromQuery(pos, limit) {
|
|
|
121
135
|
* or the API has another page.
|
|
122
136
|
*/
|
|
123
137
|
export function fromPage(rows, pos, limit, apiHasMore) {
|
|
124
|
-
const seen = new Set(pos.ids);
|
|
125
|
-
const fresh = rows.filter((r) => r.Id === undefined || !seen.has(r.Id));
|
|
138
|
+
const seen = new Set(pos.ids.map(idKey));
|
|
139
|
+
const fresh = rows.filter((r) => r.Id === undefined || !seen.has(idKey(r.Id)));
|
|
126
140
|
const kept = fresh.slice(0, limit);
|
|
127
141
|
const next = nextPosition(kept, pos);
|
|
128
142
|
return { rows: kept, next_from: encodeFrom(next), has_more: fresh.length > limit || apiHasMore };
|
|
129
143
|
}
|
|
130
144
|
function nextPosition(rows, pos) {
|
|
131
145
|
const dated = rows
|
|
132
|
-
.map((r) => ({ id: r.Id, ms: r.ReceivedDateTime ? Date.parse(r.ReceivedDateTime) : NaN }))
|
|
146
|
+
.map((r) => ({ id: r.Id === undefined ? undefined : idKey(r.Id), ms: r.ReceivedDateTime ? Date.parse(r.ReceivedDateTime) : NaN }))
|
|
133
147
|
.filter((r) => r.id !== undefined && !Number.isNaN(r.ms));
|
|
134
148
|
if (dated.length === 0)
|
|
135
149
|
return pos;
|
|
136
150
|
const newest = Math.max(...dated.map((r) => r.ms));
|
|
137
151
|
const tied = dated.filter((r) => r.ms === newest).map((r) => r.id);
|
|
138
152
|
// The position did not move: earlier ties at this instant stay excluded.
|
|
139
|
-
const carried = newest === Date.parse(pos.t) ? pos.ids.filter((id) => !tied.includes(id)) : [];
|
|
153
|
+
const carried = newest === Date.parse(pos.t) ? pos.ids.map(idKey).filter((id) => !tied.includes(id)) : [];
|
|
140
154
|
return { t: utcLiteral(newest), ids: [...carried, ...tied] };
|
|
141
155
|
}
|
package/dist/tools/export.js
CHANGED
|
@@ -158,8 +158,15 @@ const eventArgsSchema = z.object({
|
|
|
158
158
|
format: z.enum(['jsonl', 'markdown']),
|
|
159
159
|
attachments: z.boolean().optional(),
|
|
160
160
|
});
|
|
161
|
-
/**
|
|
161
|
+
/**
|
|
162
|
+
* Where the next part starts: a keyset position, or a nextLink page + rows
|
|
163
|
+
* consumed from it. The keyset position is kept as plain `{ t, ids }` (`at`):
|
|
164
|
+
* the cursor is base64'd as a whole, so a nested `next_from` token would be
|
|
165
|
+
* encoded twice. `from` (that nested token) is still read from cursors an
|
|
166
|
+
* earlier release handed out.
|
|
167
|
+
*/
|
|
162
168
|
const positionSchema = z.union([
|
|
169
|
+
z.object({ at: z.object({ t: z.string().min(1), ids: z.array(z.string()) }) }),
|
|
163
170
|
z.object({ from: z.string().min(1) }),
|
|
164
171
|
z.object({ link: z.string().min(1).optional(), offset: z.number().int().min(0).max(MAX_TOP) }),
|
|
165
172
|
]);
|
|
@@ -877,12 +884,20 @@ export function registerExportTools(server, client, opts = {}) {
|
|
|
877
884
|
const folder = args.search !== undefined ? 'all' : (args.folder ?? 'inbox');
|
|
878
885
|
const path = folder === 'all' ? '/me/messages' : `/me/mailfolders/${encodeURIComponent(folder)}/messages`;
|
|
879
886
|
const perPage = args.attachments === true ? Math.min(pageSize, ATTACHMENT_PAGE_SIZE) : pageSize;
|
|
880
|
-
const
|
|
881
|
-
|
|
882
|
-
|
|
887
|
+
const stored = resumed?.pos;
|
|
888
|
+
// Validate (and normalise) a cursor's keyset position before any work; read an older cursor's nested token.
|
|
889
|
+
const pos = stored === undefined
|
|
890
|
+
? args.search !== undefined
|
|
891
|
+
? { offset: 0 }
|
|
892
|
+
: { at: { t: since ?? EPOCH, ids: [] } }
|
|
893
|
+
: 'from' in stored
|
|
894
|
+
? { at: decodeFrom(stored.from) }
|
|
895
|
+
: 'at' in stored
|
|
896
|
+
? { at: decodeFrom(encodeFrom(stored.at)) }
|
|
897
|
+
: stored;
|
|
883
898
|
const fetch = async (p, remaining) => {
|
|
884
|
-
if ('
|
|
885
|
-
const at =
|
|
899
|
+
if ('at' in p) {
|
|
900
|
+
const { at } = p;
|
|
886
901
|
const limit = Math.min(remaining, perPage);
|
|
887
902
|
const { bound, top } = fromQuery(at, limit);
|
|
888
903
|
const filter = until ? `${bound} and ReceivedDateTime lt ${until}` : bound;
|
|
@@ -900,7 +915,7 @@ export function registerExportTools(server, client, opts = {}) {
|
|
|
900
915
|
posAfter: () => p,
|
|
901
916
|
};
|
|
902
917
|
}
|
|
903
|
-
if (page.has_more && page.next_from ===
|
|
918
|
+
if (page.has_more && page.next_from === encodeFrom(at)) {
|
|
904
919
|
// No row carried a usable ReceivedDateTime, so the position did not
|
|
905
920
|
// move; asking again would return (and duplicate) the same page forever.
|
|
906
921
|
return {
|
|
@@ -913,10 +928,12 @@ export function registerExportTools(server, client, opts = {}) {
|
|
|
913
928
|
}
|
|
914
929
|
return {
|
|
915
930
|
rows: page.rows,
|
|
916
|
-
pos: {
|
|
931
|
+
pos: { at: decodeFrom(page.next_from) },
|
|
917
932
|
hasMore: page.has_more,
|
|
918
933
|
// The first n rows are fresh and ascending, so the keyset position after them is computed the same way.
|
|
919
|
-
posAfter: (n) => n >= page.rows.length
|
|
934
|
+
posAfter: (n) => n >= page.rows.length
|
|
935
|
+
? { at: decodeFrom(page.next_from) }
|
|
936
|
+
: { at: decodeFrom(fromPage(page.rows.slice(0, n), at, n, true).next_from) },
|
|
920
937
|
};
|
|
921
938
|
}
|
|
922
939
|
return linkBatch(p, remaining, () => with429Retry(() => client.get(path, {
|
|
@@ -1010,9 +1027,10 @@ export function registerExportTools(server, client, opts = {}) {
|
|
|
1010
1027
|
}
|
|
1011
1028
|
const prefer = args.time_zone ? `outlook.timezone="${args.time_zone}"` : undefined;
|
|
1012
1029
|
const path = args.calendar !== undefined ? `/me/calendars/${encodeURIComponent(args.calendar)}/calendarview` : '/me/calendarview';
|
|
1013
|
-
const
|
|
1014
|
-
if ('
|
|
1030
|
+
const stored = resumed?.pos ?? { offset: 0 };
|
|
1031
|
+
if (!('offset' in stored))
|
|
1015
1032
|
throw new McpToolError('`cursor` is not a valid export cursor for this tool.');
|
|
1033
|
+
const pos = stored;
|
|
1016
1034
|
const fetch = (p, remaining) => linkBatch(p, remaining, () => with429Retry(() => client.get(path, {
|
|
1017
1035
|
text: true,
|
|
1018
1036
|
...(prefer ? { prefer } : {}),
|
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.8.
|
|
3
|
+
"version": "0.8.3",
|
|
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.8.
|
|
9
|
+
"version": "0.8.3",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "@chrischall/office-outlook-mcp",
|
|
14
|
-
"version": "0.8.
|
|
14
|
+
"version": "0.8.3",
|
|
15
15
|
"transport": {
|
|
16
16
|
"type": "stdio"
|
|
17
17
|
},
|