@chrischall/office-outlook-mcp 0.8.2 → 0.8.4
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/README.md +29 -1
- package/dist/bundle.js +2226 -1741
- package/dist/tools/_paging.js +19 -5
- package/dist/tools/export.js +29 -11
- package/dist/tools/mail.js +90 -19
- package/dist/version.js +1 -1
- package/package.json +2 -2
- package/server.json +2 -2
package/dist/bundle.js
CHANGED
|
@@ -24110,22 +24110,22 @@ function inputResponse(responses, key) {
|
|
|
24110
24110
|
if (responses === void 0 || typeof responses !== "object" || responses === null) return { kind: "missing" };
|
|
24111
24111
|
const entry = responses[key];
|
|
24112
24112
|
if (entry === null || typeof entry !== "object" || Array.isArray(entry)) return { kind: "missing" };
|
|
24113
|
-
const
|
|
24114
|
-
if (
|
|
24115
|
-
const content =
|
|
24113
|
+
const candidate2 = entry;
|
|
24114
|
+
if (candidate2["action"] === "accept" || candidate2["action"] === "decline" || candidate2["action"] === "cancel") {
|
|
24115
|
+
const content = candidate2["content"];
|
|
24116
24116
|
return {
|
|
24117
24117
|
kind: "elicit",
|
|
24118
|
-
action:
|
|
24118
|
+
action: candidate2["action"],
|
|
24119
24119
|
...content !== null && typeof content === "object" && !Array.isArray(content) && { content }
|
|
24120
24120
|
};
|
|
24121
24121
|
}
|
|
24122
|
-
if (Array.isArray(
|
|
24122
|
+
if (Array.isArray(candidate2["roots"])) return {
|
|
24123
24123
|
kind: "roots",
|
|
24124
|
-
roots:
|
|
24124
|
+
roots: candidate2["roots"]
|
|
24125
24125
|
};
|
|
24126
|
-
if (typeof
|
|
24126
|
+
if (typeof candidate2["role"] === "string" && candidate2["content"] !== void 0) return {
|
|
24127
24127
|
kind: "sampling",
|
|
24128
|
-
result:
|
|
24128
|
+
result: candidate2
|
|
24129
24129
|
};
|
|
24130
24130
|
return { kind: "missing" };
|
|
24131
24131
|
}
|
|
@@ -33788,8 +33788,8 @@ function createCompletionResult(suggestions) {
|
|
|
33788
33788
|
} };
|
|
33789
33789
|
}
|
|
33790
33790
|
function getSchemaShape(schema) {
|
|
33791
|
-
const
|
|
33792
|
-
if (
|
|
33791
|
+
const candidate2 = schema;
|
|
33792
|
+
if (candidate2.shape && typeof candidate2.shape === "object") return candidate2.shape;
|
|
33793
33793
|
}
|
|
33794
33794
|
function isOptionalSchema(schema) {
|
|
33795
33795
|
return schema?.type === "optional";
|
|
@@ -36348,11 +36348,6 @@ function textResult(data) {
|
|
|
36348
36348
|
content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
|
|
36349
36349
|
};
|
|
36350
36350
|
}
|
|
36351
|
-
function imageResult(base643, mimeType) {
|
|
36352
|
-
return {
|
|
36353
|
-
content: [{ type: "image", data: base643, mimeType }]
|
|
36354
|
-
};
|
|
36355
|
-
}
|
|
36356
36351
|
function errorResult(message) {
|
|
36357
36352
|
return {
|
|
36358
36353
|
content: [{ type: "text", text: redactSecrets(message) }],
|
|
@@ -37569,7 +37564,33 @@ function sanitizeBaseName(base) {
|
|
|
37569
37564
|
function isSafeExtension(ext) {
|
|
37570
37565
|
return SAFE_EXTENSION.test(ext);
|
|
37571
37566
|
}
|
|
37572
|
-
|
|
37567
|
+
function assertSafeExtension(ext, caller) {
|
|
37568
|
+
if (!isSafeExtension(ext)) {
|
|
37569
|
+
throw new TypeError(`${caller}: extension must be 1-16 characters of A-Z a-z 0-9 . _ - starting with a letter or digit, got ${JSON.stringify(ext)}.`);
|
|
37570
|
+
}
|
|
37571
|
+
}
|
|
37572
|
+
function candidate(dir, safeBase, ext, n) {
|
|
37573
|
+
return join4(dir, n === 1 ? `${safeBase}.${ext}` : `${safeBase}-${n}.${ext}`);
|
|
37574
|
+
}
|
|
37575
|
+
async function writeUniqueFile(opts) {
|
|
37576
|
+
assertSafeExtension(opts.extension, "writeUniqueFile");
|
|
37577
|
+
const dir = confine(opts.dir, opts.allowedRoots);
|
|
37578
|
+
await mkdir(dir, { recursive: true });
|
|
37579
|
+
const realDir = confine(dir, opts.allowedRoots);
|
|
37580
|
+
const safe = sanitizeBaseName(opts.baseName);
|
|
37581
|
+
const max = opts.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;
|
|
37582
|
+
for (let n = 1; ; n++) {
|
|
37583
|
+
const path = candidate(realDir, safe, opts.extension, n);
|
|
37584
|
+
try {
|
|
37585
|
+
return await writeFileSafe(path, opts.bytes, opts.mode !== void 0 ? { mode: opts.mode } : {});
|
|
37586
|
+
} catch (err) {
|
|
37587
|
+
const taken = err instanceof FileWriteRefusedError && (err.reason === "exists" || err.reason === "symlink");
|
|
37588
|
+
if (!taken || n >= max)
|
|
37589
|
+
throw err;
|
|
37590
|
+
}
|
|
37591
|
+
}
|
|
37592
|
+
}
|
|
37593
|
+
var O_WRONLY, O_CREAT, O_TRUNC, O_EXCL, O_NOFOLLOW, FileWriteRefusedError, SAFE_EXTENSION, DEFAULT_MAX_ATTEMPTS;
|
|
37573
37594
|
var init_write = __esm({
|
|
37574
37595
|
"node_modules/@chrischall/mcp-utils/dist/fs/write.js"() {
|
|
37575
37596
|
init_config();
|
|
@@ -37589,6 +37610,7 @@ var init_write = __esm({
|
|
|
37589
37610
|
}
|
|
37590
37611
|
};
|
|
37591
37612
|
SAFE_EXTENSION = /^[A-Za-z0-9][A-Za-z0-9._-]{0,15}$/;
|
|
37613
|
+
DEFAULT_MAX_ATTEMPTS = 1e3;
|
|
37592
37614
|
}
|
|
37593
37615
|
});
|
|
37594
37616
|
|
|
@@ -37623,18 +37645,123 @@ function defaultOutputDir(envVar, name) {
|
|
|
37623
37645
|
mkdirSync3(dir, { recursive: true, mode: 448 });
|
|
37624
37646
|
return dir;
|
|
37625
37647
|
}
|
|
37648
|
+
function extensionForMime(mime, opts = {}) {
|
|
37649
|
+
const fallback = opts.fallback ?? "bin";
|
|
37650
|
+
assertSafeExtension(fallback, "extensionForMime");
|
|
37651
|
+
const type = (mime ?? "").split(";", 1)[0].trim().toLowerCase();
|
|
37652
|
+
const known = MIME_EXT[type];
|
|
37653
|
+
if (known !== void 0)
|
|
37654
|
+
return known;
|
|
37655
|
+
const slash = type.indexOf("/");
|
|
37656
|
+
const subtype = slash > 0 && type.indexOf("/", slash + 1) < 0 ? type.slice(slash + 1).replace(/^x-/, "") : "";
|
|
37657
|
+
if (PLAIN_EXT.test(subtype))
|
|
37658
|
+
return subtype;
|
|
37659
|
+
if (opts.url !== void 0) {
|
|
37660
|
+
try {
|
|
37661
|
+
const fromUrl = /\.([a-z0-9]{1,5})$/i.exec(new URL(opts.url).pathname)?.[1]?.toLowerCase();
|
|
37662
|
+
if (fromUrl !== void 0 && isSafeExtension(fromUrl))
|
|
37663
|
+
return fromUrl;
|
|
37664
|
+
} catch {
|
|
37665
|
+
}
|
|
37666
|
+
}
|
|
37667
|
+
return fallback;
|
|
37668
|
+
}
|
|
37669
|
+
var MIME_EXT, PLAIN_EXT;
|
|
37626
37670
|
var init_output = __esm({
|
|
37627
37671
|
"node_modules/@chrischall/mcp-utils/dist/fs/output.js"() {
|
|
37628
37672
|
init_config();
|
|
37629
37673
|
init_errors3();
|
|
37630
37674
|
init_confine();
|
|
37631
37675
|
init_write();
|
|
37676
|
+
MIME_EXT = {
|
|
37677
|
+
"image/png": "png",
|
|
37678
|
+
"image/jpeg": "jpg",
|
|
37679
|
+
"image/jpg": "jpg",
|
|
37680
|
+
"image/pjpeg": "jpg",
|
|
37681
|
+
"image/webp": "webp",
|
|
37682
|
+
"image/gif": "gif",
|
|
37683
|
+
"image/heic": "heic",
|
|
37684
|
+
"image/heif": "heif",
|
|
37685
|
+
"image/avif": "avif",
|
|
37686
|
+
"image/tiff": "tif",
|
|
37687
|
+
"image/bmp": "bmp",
|
|
37688
|
+
"image/svg+xml": "svg",
|
|
37689
|
+
"application/pdf": "pdf",
|
|
37690
|
+
"application/json": "json",
|
|
37691
|
+
"application/zip": "zip",
|
|
37692
|
+
"application/gzip": "gz",
|
|
37693
|
+
"text/plain": "txt",
|
|
37694
|
+
"text/csv": "csv",
|
|
37695
|
+
"text/html": "html",
|
|
37696
|
+
"text/calendar": "ics",
|
|
37697
|
+
"audio/midi": "mid",
|
|
37698
|
+
"audio/mpeg": "mp3",
|
|
37699
|
+
"audio/mp3": "mp3",
|
|
37700
|
+
"audio/wav": "wav",
|
|
37701
|
+
"audio/wave": "wav",
|
|
37702
|
+
"audio/x-wav": "wav",
|
|
37703
|
+
"audio/ogg": "ogg",
|
|
37704
|
+
"audio/webm": "webm",
|
|
37705
|
+
"audio/mp4": "m4a",
|
|
37706
|
+
"video/mp4": "mp4",
|
|
37707
|
+
"video/quicktime": "mov",
|
|
37708
|
+
"video/webm": "webm"
|
|
37709
|
+
};
|
|
37710
|
+
PLAIN_EXT = /^[a-z0-9]{1,5}$/;
|
|
37632
37711
|
}
|
|
37633
37712
|
});
|
|
37634
37713
|
|
|
37635
37714
|
// node_modules/@chrischall/mcp-utils/dist/fs/magic.js
|
|
37715
|
+
function startsWith(buf, sig, offset = 0) {
|
|
37716
|
+
const bytes = typeof sig === "string" ? Array.from(sig, (c) => c.charCodeAt(0)) : sig;
|
|
37717
|
+
if (buf.length < offset + bytes.length)
|
|
37718
|
+
return false;
|
|
37719
|
+
for (let i = 0; i < bytes.length; i++)
|
|
37720
|
+
if (buf[offset + i] !== bytes[i])
|
|
37721
|
+
return false;
|
|
37722
|
+
return true;
|
|
37723
|
+
}
|
|
37724
|
+
function latin1(buf, start, end) {
|
|
37725
|
+
return String.fromCharCode(...buf.subarray(start, end));
|
|
37726
|
+
}
|
|
37727
|
+
function bmffMime(buf) {
|
|
37728
|
+
if (buf.length < 12 || latin1(buf, 4, 8) !== "ftyp")
|
|
37729
|
+
return void 0;
|
|
37730
|
+
const brand = latin1(buf, 8, 12);
|
|
37731
|
+
if (HEIC_BRANDS.has(brand))
|
|
37732
|
+
return "image/heic";
|
|
37733
|
+
if (HEIF_BRANDS.has(brand))
|
|
37734
|
+
return "image/heif";
|
|
37735
|
+
if (brand === "avif" || brand === "avis")
|
|
37736
|
+
return "image/avif";
|
|
37737
|
+
if (brand === "qt ")
|
|
37738
|
+
return "video/quicktime";
|
|
37739
|
+
if (brand === "M4A ")
|
|
37740
|
+
return "audio/mp4";
|
|
37741
|
+
return "video/mp4";
|
|
37742
|
+
}
|
|
37743
|
+
function sniffMimeBytes(bytes) {
|
|
37744
|
+
for (const [mime, test] of SIGNATURES)
|
|
37745
|
+
if (test(bytes))
|
|
37746
|
+
return mime;
|
|
37747
|
+
return bmffMime(bytes);
|
|
37748
|
+
}
|
|
37749
|
+
var PNG_SIG, HEIC_BRANDS, HEIF_BRANDS, SIGNATURES;
|
|
37636
37750
|
var init_magic = __esm({
|
|
37637
37751
|
"node_modules/@chrischall/mcp-utils/dist/fs/magic.js"() {
|
|
37752
|
+
PNG_SIG = [137, 80, 78, 71, 13, 10, 26, 10];
|
|
37753
|
+
HEIC_BRANDS = /* @__PURE__ */ new Set(["heic", "heix", "heim", "heis", "hevc", "hevx"]);
|
|
37754
|
+
HEIF_BRANDS = /* @__PURE__ */ new Set(["mif1", "msf1"]);
|
|
37755
|
+
SIGNATURES = [
|
|
37756
|
+
["image/png", (b) => startsWith(b, PNG_SIG)],
|
|
37757
|
+
["image/jpeg", (b) => startsWith(b, [255, 216, 255])],
|
|
37758
|
+
["image/webp", (b) => startsWith(b, "RIFF") && startsWith(b, "WEBP", 8)],
|
|
37759
|
+
["image/gif", (b) => startsWith(b, "GIF8")],
|
|
37760
|
+
["application/pdf", (b) => startsWith(b, "%PDF-")],
|
|
37761
|
+
// Local file header; empty archive (end-of-central-directory first); spanned.
|
|
37762
|
+
["application/zip", (b) => startsWith(b, [80, 75, 3, 4]) || startsWith(b, [80, 75, 5, 6]) || startsWith(b, [80, 75, 7, 8])],
|
|
37763
|
+
["audio/midi", (b) => startsWith(b, "MThd")]
|
|
37764
|
+
];
|
|
37638
37765
|
}
|
|
37639
37766
|
});
|
|
37640
37767
|
|
|
@@ -37826,6 +37953,86 @@ var init_body_limit = __esm({
|
|
|
37826
37953
|
});
|
|
37827
37954
|
|
|
37828
37955
|
// node_modules/@chrischall/mcp-utils/dist/http/fetch-bounded.js
|
|
37956
|
+
async function fetchBounded(url2, init, opts = {}) {
|
|
37957
|
+
const doFetch = opts.fetchImpl ?? fetch;
|
|
37958
|
+
const service = opts.service ?? hostOf(url2);
|
|
37959
|
+
const read = opts.read ?? "text";
|
|
37960
|
+
const timeoutMs = opts.timeoutMs === false ? 0 : opts.timeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
37961
|
+
const caller = withAmbientCancellation(init?.signal ?? void 0);
|
|
37962
|
+
if (caller?.aborted)
|
|
37963
|
+
throw reasonOf(caller);
|
|
37964
|
+
const timer = timeoutMs > 0 ? new AbortController() : void 0;
|
|
37965
|
+
const signal = combine(timer?.signal, caller);
|
|
37966
|
+
let handle;
|
|
37967
|
+
let detach = () => {
|
|
37968
|
+
};
|
|
37969
|
+
const stop = signal ? new Promise((_resolve, reject) => {
|
|
37970
|
+
const onAbort = () => reject(timer?.signal.aborted ? new RequestTimeoutError(service, timeoutMs) : reasonOf(caller));
|
|
37971
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
37972
|
+
detach = () => signal.removeEventListener("abort", onAbort);
|
|
37973
|
+
}) : void 0;
|
|
37974
|
+
stop?.catch(() => {
|
|
37975
|
+
});
|
|
37976
|
+
const bounded = (p) => stop ? Promise.race([p, stop]) : p;
|
|
37977
|
+
if (timer)
|
|
37978
|
+
handle = setTimeout(() => timer.abort(), timeoutMs);
|
|
37979
|
+
try {
|
|
37980
|
+
let res;
|
|
37981
|
+
try {
|
|
37982
|
+
res = await bounded(doFetch(url2, { ...init, ...signal ? { signal } : {} }));
|
|
37983
|
+
} catch (err) {
|
|
37984
|
+
if (timer?.signal.aborted)
|
|
37985
|
+
throw new RequestTimeoutError(service, timeoutMs);
|
|
37986
|
+
if (caller?.aborted)
|
|
37987
|
+
throw reasonOf(caller);
|
|
37988
|
+
throw err;
|
|
37989
|
+
}
|
|
37990
|
+
if (read === "none") {
|
|
37991
|
+
cancelStream(res.body);
|
|
37992
|
+
return done(res, void 0);
|
|
37993
|
+
}
|
|
37994
|
+
const bytes = await readBytesCapped(res, opts.maxBytes, service, bounded);
|
|
37995
|
+
if (read === "bytes")
|
|
37996
|
+
return done(res, bytes);
|
|
37997
|
+
const text = new TextDecoder().decode(bytes);
|
|
37998
|
+
if (read === "text")
|
|
37999
|
+
return done(res, text);
|
|
38000
|
+
if (text.length === 0)
|
|
38001
|
+
return done(res, void 0);
|
|
38002
|
+
try {
|
|
38003
|
+
return done(res, JSON.parse(text));
|
|
38004
|
+
} catch (cause) {
|
|
38005
|
+
const type = res.headers.get("content-type") ?? "no content-type";
|
|
38006
|
+
throw new McpToolError(`${service} returned a non-JSON response (HTTP ${res.status}, ${type}).`, {
|
|
38007
|
+
hint: "The upstream may have served an error or sign-in page instead of data.",
|
|
38008
|
+
cause,
|
|
38009
|
+
status: res.status
|
|
38010
|
+
});
|
|
38011
|
+
}
|
|
38012
|
+
} finally {
|
|
38013
|
+
clearTimeout(handle);
|
|
38014
|
+
detach();
|
|
38015
|
+
}
|
|
38016
|
+
function done(res, body) {
|
|
38017
|
+
return { res, status: res.status, ok: res.ok, headers: res.headers, body };
|
|
38018
|
+
}
|
|
38019
|
+
}
|
|
38020
|
+
function combine(a, b) {
|
|
38021
|
+
if (a && b)
|
|
38022
|
+
return AbortSignal.any([a, b]);
|
|
38023
|
+
return a ?? b;
|
|
38024
|
+
}
|
|
38025
|
+
function reasonOf(signal) {
|
|
38026
|
+
const reason = signal.reason;
|
|
38027
|
+
return reason instanceof Error ? reason : new Error(String(reason));
|
|
38028
|
+
}
|
|
38029
|
+
function hostOf(url2) {
|
|
38030
|
+
try {
|
|
38031
|
+
return new URL(url2).host;
|
|
38032
|
+
} catch {
|
|
38033
|
+
return String(url2);
|
|
38034
|
+
}
|
|
38035
|
+
}
|
|
37829
38036
|
var init_fetch_bounded = __esm({
|
|
37830
38037
|
"node_modules/@chrischall/mcp-utils/dist/http/fetch-bounded.js"() {
|
|
37831
38038
|
init_cancel();
|
|
@@ -38352,7 +38559,7 @@ function encodeBody(opt) {
|
|
|
38352
38559
|
return { body: JSON.stringify(opt.body), contentType: "application/json" };
|
|
38353
38560
|
return { body: void 0, contentType: void 0 };
|
|
38354
38561
|
}
|
|
38355
|
-
function
|
|
38562
|
+
function hostOf2(baseUrl) {
|
|
38356
38563
|
try {
|
|
38357
38564
|
return new URL(baseUrl).host;
|
|
38358
38565
|
} catch {
|
|
@@ -38362,7 +38569,7 @@ function hostOf(baseUrl) {
|
|
|
38362
38569
|
function createApiClient(opts) {
|
|
38363
38570
|
const base = opts.baseUrl.replace(/\/+$/, "");
|
|
38364
38571
|
const retry = opts.retry ?? DEFAULT_RETRY;
|
|
38365
|
-
const service = opts.serviceName ??
|
|
38572
|
+
const service = opts.serviceName ?? hostOf2(opts.baseUrl);
|
|
38366
38573
|
const doFetch = opts.fetchImpl ?? fetch;
|
|
38367
38574
|
const sleep3 = (ms) => cancellableSleep(ms, opts.sleep);
|
|
38368
38575
|
const unauthorized = () => opts.onUnauthorized ? opts.onUnauthorized() : new UnauthorizedError(service);
|
|
@@ -39086,7 +39293,7 @@ var init_version = __esm({
|
|
|
39086
39293
|
"src/version.ts"() {
|
|
39087
39294
|
"use strict";
|
|
39088
39295
|
PACKAGE_NAME = "office-outlook-mcp";
|
|
39089
|
-
VERSION = "0.8.
|
|
39296
|
+
VERSION = "0.8.4";
|
|
39090
39297
|
}
|
|
39091
39298
|
});
|
|
39092
39299
|
|
|
@@ -43398,7 +43605,7 @@ var require_websocket = __commonJS({
|
|
|
43398
43605
|
var http = __require("http");
|
|
43399
43606
|
var net = __require("net");
|
|
43400
43607
|
var tls = __require("tls");
|
|
43401
|
-
var { randomBytes: randomBytes8, createHash:
|
|
43608
|
+
var { randomBytes: randomBytes8, createHash: createHash6 } = __require("crypto");
|
|
43402
43609
|
var { Duplex, Readable } = __require("stream");
|
|
43403
43610
|
var { URL: URL2 } = __require("url");
|
|
43404
43611
|
var PerMessageDeflate2 = require_permessage_deflate();
|
|
@@ -44079,7 +44286,7 @@ var require_websocket = __commonJS({
|
|
|
44079
44286
|
abortHandshake(websocket, socket, "Invalid Upgrade header");
|
|
44080
44287
|
return;
|
|
44081
44288
|
}
|
|
44082
|
-
const digest =
|
|
44289
|
+
const digest = createHash6("sha1").update(key + GUID).digest("base64");
|
|
44083
44290
|
if (res.headers["sec-websocket-accept"] !== digest) {
|
|
44084
44291
|
abortHandshake(websocket, socket, "Invalid Sec-WebSocket-Accept header");
|
|
44085
44292
|
return;
|
|
@@ -44448,7 +44655,7 @@ var require_websocket_server = __commonJS({
|
|
|
44448
44655
|
var EventEmitter = __require("events");
|
|
44449
44656
|
var http = __require("http");
|
|
44450
44657
|
var { Duplex } = __require("stream");
|
|
44451
|
-
var { createHash:
|
|
44658
|
+
var { createHash: createHash6 } = __require("crypto");
|
|
44452
44659
|
var extension2 = require_extension();
|
|
44453
44660
|
var PerMessageDeflate2 = require_permessage_deflate();
|
|
44454
44661
|
var subprotocol2 = require_subprotocol();
|
|
@@ -44755,7 +44962,7 @@ var require_websocket_server = __commonJS({
|
|
|
44755
44962
|
);
|
|
44756
44963
|
}
|
|
44757
44964
|
if (this._state > RUNNING) return abortHandshake(socket, 503);
|
|
44758
|
-
const digest =
|
|
44965
|
+
const digest = createHash6("sha1").update(key + GUID).digest("base64");
|
|
44759
44966
|
const headers = [
|
|
44760
44967
|
"HTTP/1.1 101 Switching Protocols",
|
|
44761
44968
|
"Upgrade: websocket",
|
|
@@ -50418,419 +50625,1273 @@ var OutlookClient = class {
|
|
|
50418
50625
|
// src/tools/mail.ts
|
|
50419
50626
|
init_zod();
|
|
50420
50627
|
init_dist2();
|
|
50628
|
+
import { existsSync as existsSync6 } from "node:fs";
|
|
50629
|
+
import { homedir as homedir6 } from "node:os";
|
|
50630
|
+
import { join as join11 } from "node:path";
|
|
50421
50631
|
|
|
50422
|
-
//
|
|
50423
|
-
|
|
50424
|
-
|
|
50425
|
-
|
|
50426
|
-
|
|
50427
|
-
|
|
50428
|
-
|
|
50429
|
-
var
|
|
50430
|
-
|
|
50431
|
-
|
|
50432
|
-
|
|
50433
|
-
|
|
50434
|
-
|
|
50435
|
-
|
|
50436
|
-
|
|
50437
|
-
}
|
|
50438
|
-
var BRIEF_MAX = 120;
|
|
50439
|
-
var BRIEF_NOTE = '"brief": one line per item, `title \u2014 <ISO date or "no date"> \u2014 <status>`, at most 120 chars each, as `data` in the untrusted-content envelope; a line starting `-- ` is a note (`-- none` for an empty page; a title starting `-- ` or `\\` gets one leading `\\`); a last line `-- more` means another page exists (re-ask without "brief" for its nextLink).';
|
|
50440
|
-
function briefViewParam(rungs, note) {
|
|
50441
|
-
const base = viewParam(rungs);
|
|
50442
|
-
return external_exports.enum([...rungs, "brief"]).optional().describe(`${base.description ?? ""} ${BRIEF_NOTE} ${note}`.trim());
|
|
50443
|
-
}
|
|
50444
|
-
function flat(s) {
|
|
50445
|
-
const out = s?.replace(/\s+/g, " ").trim();
|
|
50446
|
-
return out ? out : void 0;
|
|
50632
|
+
// node_modules/@chrischall/mcp-utils/dist/export/blob.js
|
|
50633
|
+
init_config();
|
|
50634
|
+
init_errors3();
|
|
50635
|
+
init_http();
|
|
50636
|
+
import { createHmac as createHmac4 } from "node:crypto";
|
|
50637
|
+
var BLOB_DEFAULT_TTL_MS = 60 * 60 * 1e3;
|
|
50638
|
+
var BLOB_MAX_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
50639
|
+
var BLOB_MAX_OBJECT_BYTES = 100 * 1024 * 1024;
|
|
50640
|
+
var MAX_KEY_LENGTH = 1024;
|
|
50641
|
+
var SAFE_SEGMENT = /^[A-Za-z0-9._~-]+$/;
|
|
50642
|
+
function configError(reason) {
|
|
50643
|
+
return new McpToolError(`MCP_BLOB_BASE_URL is not usable: ${reason}.`, {
|
|
50644
|
+
hint: "It should look like https://<host>/b/<registrationId> \u2014 check the deployment's environment.",
|
|
50645
|
+
kind: "unknown"
|
|
50646
|
+
});
|
|
50447
50647
|
}
|
|
50448
|
-
function
|
|
50449
|
-
|
|
50648
|
+
function parseBase(baseUrl) {
|
|
50649
|
+
let parsed;
|
|
50650
|
+
try {
|
|
50651
|
+
parsed = new URL(baseUrl);
|
|
50652
|
+
} catch {
|
|
50653
|
+
throw configError("not a valid URL");
|
|
50654
|
+
}
|
|
50655
|
+
if (parsed.protocol !== "https:" && parsed.protocol !== "http:")
|
|
50656
|
+
throw configError("scheme must be http(s)");
|
|
50657
|
+
if (parsed.username || parsed.password)
|
|
50658
|
+
throw configError("must not contain credentials");
|
|
50659
|
+
if (parsed.search || parsed.hash)
|
|
50660
|
+
throw configError("must not contain a query or fragment");
|
|
50661
|
+
const path = parsed.pathname.replace(/^\/+|\/+$/g, "");
|
|
50662
|
+
const prefix = path.startsWith("b/") ? path.slice(2) : path;
|
|
50663
|
+
if (!prefix || prefix === "b")
|
|
50664
|
+
throw configError("has no registration path");
|
|
50665
|
+
return { base: `${parsed.origin}/${path}`, prefix };
|
|
50450
50666
|
}
|
|
50451
|
-
|
|
50452
|
-
|
|
50453
|
-
function briefLine(title, date5, status) {
|
|
50454
|
-
const raw = flat(title) ?? "(no subject)";
|
|
50455
|
-
const shown = raw.startsWith("-- ") || raw.startsWith("\\") ? `\\${raw}` : raw;
|
|
50456
|
-
const sep2 = ` \u2014 ${date5} \u2014 `;
|
|
50457
|
-
const room = BRIEF_MAX - sep2.length;
|
|
50458
|
-
if (shown.length + status.length <= room) return `${shown}${sep2}${status}`;
|
|
50459
|
-
const titleRoom = Math.max(room - status.length, Math.min(shown.length, TITLE_FLOOR));
|
|
50460
|
-
const t = cut(shown, titleRoom);
|
|
50461
|
-
return cut(`${t}${sep2}${cut(status, room - t.length)}`, BRIEF_MAX);
|
|
50667
|
+
function badInput(message) {
|
|
50668
|
+
return new McpToolError(message, { kind: "unknown" });
|
|
50462
50669
|
}
|
|
50463
|
-
|
|
50464
|
-
|
|
50465
|
-
|
|
50466
|
-
|
|
50467
|
-
|
|
50670
|
+
function encodeKey(key) {
|
|
50671
|
+
if (typeof key !== "string" || key.length === 0)
|
|
50672
|
+
throw badInput("Blob key must be a non-empty string.");
|
|
50673
|
+
if (key.length > MAX_KEY_LENGTH)
|
|
50674
|
+
throw badInput(`Blob key is longer than ${MAX_KEY_LENGTH} characters.`);
|
|
50675
|
+
const segments = key.split("/");
|
|
50676
|
+
for (const seg of segments) {
|
|
50677
|
+
if (seg === "" || seg === "." || seg === ".." || !SAFE_SEGMENT.test(seg)) {
|
|
50678
|
+
throw badInput('Blob key must be relative, slash-separated segments of [A-Za-z0-9._~-] with no empty, "." or ".." segment.');
|
|
50679
|
+
}
|
|
50680
|
+
}
|
|
50681
|
+
return segments.map(encodeURIComponent).join("/");
|
|
50468
50682
|
}
|
|
50469
|
-
function
|
|
50470
|
-
|
|
50683
|
+
function checkContentType(contentType) {
|
|
50684
|
+
if (typeof contentType !== "string" || contentType.length === 0 || /[\x00-\x1f\x7f]/.test(contentType)) {
|
|
50685
|
+
throw badInput("Blob content type must be a non-empty string without control characters.");
|
|
50686
|
+
}
|
|
50471
50687
|
}
|
|
50472
|
-
|
|
50473
|
-
|
|
50474
|
-
|
|
50475
|
-
|
|
50476
|
-
|
|
50477
|
-
|
|
50478
|
-
if (!e) return void 0;
|
|
50479
|
-
if (e.Name && e.Address && e.Name !== e.Address) return `${e.Name} <${e.Address}>`;
|
|
50480
|
-
return e.Address ?? e.Name;
|
|
50688
|
+
function expiry(opts) {
|
|
50689
|
+
const ttl = opts?.ttlMs ?? BLOB_DEFAULT_TTL_MS;
|
|
50690
|
+
if (!Number.isFinite(ttl) || ttl <= 0 || ttl > BLOB_MAX_TTL_MS) {
|
|
50691
|
+
throw badInput(`Blob link TTL must be between 1 ms and ${BLOB_MAX_TTL_MS} ms (24h); the gateway refuses longer.`);
|
|
50692
|
+
}
|
|
50693
|
+
return Math.floor((opts?.now ?? Date.now()) + ttl);
|
|
50481
50694
|
}
|
|
50482
|
-
function
|
|
50483
|
-
|
|
50484
|
-
|
|
50695
|
+
function toArrayBuffer(view) {
|
|
50696
|
+
const buf = view.buffer;
|
|
50697
|
+
if (buf instanceof ArrayBuffer && view.byteOffset === 0 && view.byteLength === buf.byteLength)
|
|
50698
|
+
return buf;
|
|
50699
|
+
return Uint8Array.prototype.slice.call(view).buffer;
|
|
50485
50700
|
}
|
|
50486
|
-
|
|
50487
|
-
|
|
50488
|
-
|
|
50489
|
-
|
|
50490
|
-
|
|
50491
|
-
|
|
50492
|
-
|
|
50701
|
+
function createBlobStore(opts) {
|
|
50702
|
+
const { base, prefix } = parseBase(opts.baseUrl);
|
|
50703
|
+
const signingKey = opts.signingKey;
|
|
50704
|
+
const sign2 = (payload) => createHmac4("sha256", signingKey).update(payload).digest("base64url");
|
|
50705
|
+
const link2 = (encodedKey, exp, sig) => `${base}/${encodedKey}?exp=${exp}&sig=${encodeURIComponent(sig)}`;
|
|
50706
|
+
const signGetUrl = (key, o) => {
|
|
50707
|
+
const encoded = encodeKey(key);
|
|
50708
|
+
const exp = expiry(o);
|
|
50709
|
+
return { url: link2(encoded, exp, sign2(`${prefix}/${key}
|
|
50710
|
+
${exp}`)), expiresAtMs: exp };
|
|
50711
|
+
};
|
|
50712
|
+
const signPutUrl = (key, contentType, o) => {
|
|
50713
|
+
const encoded = encodeKey(key);
|
|
50714
|
+
checkContentType(contentType);
|
|
50715
|
+
const exp = expiry(o);
|
|
50716
|
+
return { url: link2(encoded, exp, sign2(`put\0${prefix}/${key}\0${contentType}\0${exp}`)), expiresAtMs: exp };
|
|
50717
|
+
};
|
|
50718
|
+
const put = async (key, body, contentType, o = {}) => {
|
|
50719
|
+
const { url: url2 } = signPutUrl(key, contentType, o);
|
|
50720
|
+
const bytes = typeof body === "string" ? new TextEncoder().encode(body) : body;
|
|
50721
|
+
const doFetch = o.fetchImpl ?? fetch;
|
|
50722
|
+
let res;
|
|
50723
|
+
try {
|
|
50724
|
+
res = await doFetch(url2, {
|
|
50725
|
+
method: "PUT",
|
|
50726
|
+
headers: { "content-type": contentType },
|
|
50727
|
+
body: toArrayBuffer(bytes),
|
|
50728
|
+
...o.signal ? { signal: o.signal } : {}
|
|
50729
|
+
});
|
|
50730
|
+
} catch (cause) {
|
|
50731
|
+
if (o.signal?.aborted)
|
|
50732
|
+
throw cause;
|
|
50733
|
+
throw new McpToolError(`Blob store upload of ${key} did not complete (network error).`, {
|
|
50734
|
+
kind: "transport"
|
|
50735
|
+
});
|
|
50736
|
+
}
|
|
50737
|
+
if (!res.ok) {
|
|
50738
|
+
throw new McpToolError(`Blob store upload of ${key} failed (HTTP ${res.status}).`, {
|
|
50739
|
+
kind: "http",
|
|
50740
|
+
status: res.status
|
|
50741
|
+
});
|
|
50742
|
+
}
|
|
50743
|
+
return { key, bytes: bytes.byteLength };
|
|
50744
|
+
};
|
|
50745
|
+
const signDeleteUrl = (key, o) => {
|
|
50746
|
+
const encoded = encodeKey(key);
|
|
50747
|
+
const exp = expiry(o);
|
|
50748
|
+
return { url: link2(encoded, exp, sign2(`del\0${prefix}/${key}\0${exp}`)), expiresAtMs: exp };
|
|
50749
|
+
};
|
|
50750
|
+
const exchange = async (what, url2, method, read, o) => {
|
|
50751
|
+
try {
|
|
50752
|
+
return await fetchBounded(url2, { method, ...o.signal ? { signal: o.signal } : {} }, {
|
|
50753
|
+
read,
|
|
50754
|
+
service: "the blob store",
|
|
50755
|
+
...read === "bytes" ? { maxBytes: o.maxBytes ?? BLOB_MAX_OBJECT_BYTES } : {},
|
|
50756
|
+
...o.timeoutMs !== void 0 ? { timeoutMs: o.timeoutMs } : {},
|
|
50757
|
+
...o.fetchImpl ? { fetchImpl: o.fetchImpl } : {}
|
|
50758
|
+
});
|
|
50759
|
+
} catch (cause) {
|
|
50760
|
+
if (o.signal?.aborted || cause instanceof McpToolError || cause instanceof RequestTimeoutError)
|
|
50761
|
+
throw cause;
|
|
50762
|
+
throw new McpToolError(`Blob store ${what} did not complete (network error).`, { kind: "transport" });
|
|
50763
|
+
}
|
|
50764
|
+
};
|
|
50765
|
+
const get = async (key, o = {}) => {
|
|
50766
|
+
const { url: url2 } = signGetUrl(key, o);
|
|
50767
|
+
const res = await exchange(`read of ${key}`, url2, "GET", "bytes", o);
|
|
50768
|
+
if (!res.ok) {
|
|
50769
|
+
const why = res.status === 404 ? " \u2014 not found (never uploaded, expired, or already consumed)" : "";
|
|
50770
|
+
throw new McpToolError(`Blob store read of ${key} failed (HTTP ${res.status})${why}.`, {
|
|
50771
|
+
kind: "http",
|
|
50772
|
+
status: res.status
|
|
50773
|
+
});
|
|
50774
|
+
}
|
|
50775
|
+
const contentType = res.headers.get("content-type") ?? void 0;
|
|
50776
|
+
return { key, bytes: res.body, ...contentType !== void 0 ? { contentType } : {} };
|
|
50777
|
+
};
|
|
50778
|
+
const del = async (key, o = {}) => {
|
|
50779
|
+
const { url: url2 } = signDeleteUrl(key, o);
|
|
50780
|
+
const res = await exchange(`delete of ${key}`, url2, "DELETE", "none", o);
|
|
50781
|
+
if (!res.ok) {
|
|
50782
|
+
throw new McpToolError(`Blob store delete of ${key} failed (HTTP ${res.status}).`, {
|
|
50783
|
+
kind: "http",
|
|
50784
|
+
status: res.status
|
|
50785
|
+
});
|
|
50786
|
+
}
|
|
50787
|
+
};
|
|
50788
|
+
const consume = async (key, use, o = {}) => {
|
|
50789
|
+
const object2 = await get(key, o);
|
|
50790
|
+
const result = await use(object2);
|
|
50791
|
+
try {
|
|
50792
|
+
await del(key, o);
|
|
50793
|
+
return { result, deleted: true };
|
|
50794
|
+
} catch (e) {
|
|
50795
|
+
return { result, deleted: false, deleteError: e instanceof Error ? e.message : String(e) };
|
|
50796
|
+
}
|
|
50493
50797
|
};
|
|
50798
|
+
return { baseUrl: base, prefix, signGetUrl, signPutUrl, put, signDeleteUrl, get, delete: del, consume };
|
|
50494
50799
|
}
|
|
50495
|
-
function
|
|
50496
|
-
|
|
50497
|
-
|
|
50498
|
-
|
|
50499
|
-
|
|
50500
|
-
|
|
50501
|
-
Received: m.ReceivedDateTime,
|
|
50502
|
-
IsRead: m.IsRead,
|
|
50503
|
-
HasAttachments: m.HasAttachments || void 0,
|
|
50504
|
-
Preview: m.BodyPreview?.trim() || void 0,
|
|
50505
|
-
// Present only when the caller asked for a single message; a listing
|
|
50506
|
-
// projection would otherwise carry every body.
|
|
50507
|
-
Body: m.Body?.Content
|
|
50508
|
-
});
|
|
50800
|
+
function blobStoreFromEnv(env = process.env) {
|
|
50801
|
+
const baseUrl = readEnvVar("MCP_BLOB_BASE_URL", { env });
|
|
50802
|
+
const signingKey = readEnvVar("MCP_BLOB_SIGNING_KEY", { env });
|
|
50803
|
+
if (!baseUrl || !signingKey)
|
|
50804
|
+
return void 0;
|
|
50805
|
+
return createBlobStore({ baseUrl, signingKey });
|
|
50509
50806
|
}
|
|
50510
|
-
|
|
50511
|
-
|
|
50512
|
-
|
|
50513
|
-
|
|
50514
|
-
|
|
50515
|
-
|
|
50516
|
-
|
|
50517
|
-
|
|
50518
|
-
|
|
50519
|
-
|
|
50520
|
-
|
|
50521
|
-
|
|
50807
|
+
|
|
50808
|
+
// node_modules/@chrischall/mcp-utils/dist/export/sink.js
|
|
50809
|
+
init_config();
|
|
50810
|
+
init_errors3();
|
|
50811
|
+
init_confine();
|
|
50812
|
+
init_output();
|
|
50813
|
+
init_write();
|
|
50814
|
+
import { randomBytes as randomBytes7 } from "node:crypto";
|
|
50815
|
+
import { closeSync as closeSync4, constants as fsConstants3, lstatSync as lstatSync2, mkdirSync as mkdirSync6, openSync as openSync4, readSync } from "node:fs";
|
|
50816
|
+
import { mkdir as mkdir4 } from "node:fs/promises";
|
|
50817
|
+
import { homedir as homedir5 } from "node:os";
|
|
50818
|
+
import { basename as basename2, dirname as dirname5, join as join10 } from "node:path";
|
|
50819
|
+
|
|
50820
|
+
// node_modules/@chrischall/mcp-utils/dist/export/zip.js
|
|
50821
|
+
import { deflateRawSync } from "node:zlib";
|
|
50822
|
+
var CRC_TABLE = (() => {
|
|
50823
|
+
const table = new Uint32Array(256);
|
|
50824
|
+
for (let n = 0; n < 256; n++) {
|
|
50825
|
+
let c = n;
|
|
50826
|
+
for (let k = 0; k < 8; k++)
|
|
50827
|
+
c = c & 1 ? 3988292384 ^ c >>> 1 : c >>> 1;
|
|
50828
|
+
table[n] = c >>> 0;
|
|
50829
|
+
}
|
|
50830
|
+
return table;
|
|
50831
|
+
})();
|
|
50832
|
+
function crc32(bytes) {
|
|
50833
|
+
let crc = 4294967295;
|
|
50834
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
50835
|
+
crc = CRC_TABLE[(crc ^ bytes[i]) & 255] ^ crc >>> 8;
|
|
50836
|
+
}
|
|
50837
|
+
return (crc ^ 4294967295) >>> 0;
|
|
50522
50838
|
}
|
|
50523
|
-
function
|
|
50524
|
-
|
|
50525
|
-
|
|
50526
|
-
|
|
50527
|
-
|
|
50528
|
-
|
|
50529
|
-
|
|
50530
|
-
|
|
50531
|
-
|
|
50532
|
-
IsAllDay: e.IsAllDay,
|
|
50533
|
-
IsCancelled: e.IsCancelled,
|
|
50534
|
-
IsOrganizer: e.IsOrganizer,
|
|
50535
|
-
MyResponse: e.ResponseStatus?.Response,
|
|
50536
|
-
ResponseRequested: e.ResponseRequested,
|
|
50537
|
-
Type: e.Type,
|
|
50538
|
-
ShowAs: e.ShowAs
|
|
50539
|
-
});
|
|
50839
|
+
function isCompressibleType(contentType) {
|
|
50840
|
+
const type = contentType.split(";", 1)[0].trim().toLowerCase();
|
|
50841
|
+
if (!type)
|
|
50842
|
+
return false;
|
|
50843
|
+
if (type.startsWith("text/"))
|
|
50844
|
+
return true;
|
|
50845
|
+
if (type.endsWith("+json") || type.endsWith("+xml"))
|
|
50846
|
+
return true;
|
|
50847
|
+
return COMPRESSIBLE.has(type);
|
|
50540
50848
|
}
|
|
50541
|
-
|
|
50542
|
-
|
|
50543
|
-
|
|
50544
|
-
|
|
50545
|
-
|
|
50546
|
-
|
|
50547
|
-
|
|
50548
|
-
|
|
50549
|
-
|
|
50550
|
-
|
|
50551
|
-
|
|
50552
|
-
|
|
50553
|
-
|
|
50554
|
-
|
|
50555
|
-
|
|
50849
|
+
var COMPRESSIBLE = /* @__PURE__ */ new Set([
|
|
50850
|
+
"application/json",
|
|
50851
|
+
"application/jsonl",
|
|
50852
|
+
"application/x-ndjson",
|
|
50853
|
+
"application/ndjson",
|
|
50854
|
+
"application/xml",
|
|
50855
|
+
"application/javascript",
|
|
50856
|
+
"application/x-javascript",
|
|
50857
|
+
"application/yaml",
|
|
50858
|
+
"application/x-yaml"
|
|
50859
|
+
]);
|
|
50860
|
+
function dosDateTime(at) {
|
|
50861
|
+
const year = Math.max(1980, at.getUTCFullYear());
|
|
50862
|
+
return {
|
|
50863
|
+
time: at.getUTCHours() << 11 | at.getUTCMinutes() << 5 | at.getUTCSeconds() >> 1,
|
|
50864
|
+
date: year - 1980 << 9 | at.getUTCMonth() + 1 << 5 | at.getUTCDate()
|
|
50865
|
+
};
|
|
50556
50866
|
}
|
|
50557
|
-
|
|
50558
|
-
|
|
50559
|
-
|
|
50560
|
-
|
|
50561
|
-
|
|
50562
|
-
|
|
50563
|
-
|
|
50564
|
-
|
|
50565
|
-
|
|
50867
|
+
var LOCAL_HEADER = 67324752;
|
|
50868
|
+
var CENTRAL_HEADER = 33639248;
|
|
50869
|
+
var EOCD = 101010256;
|
|
50870
|
+
var VERSION2 = 20;
|
|
50871
|
+
var FLAG_UTF8 = 2048;
|
|
50872
|
+
var METHOD_STORE = 0;
|
|
50873
|
+
var METHOD_DEFLATE = 8;
|
|
50874
|
+
var MAX_U32 = 4294967295;
|
|
50875
|
+
var MAX_ENTRIES = 65535;
|
|
50876
|
+
function checkName(name, seen) {
|
|
50877
|
+
const bad = typeof name !== "string" || name.length === 0 || name.includes("\\") || name.includes("\0") || name.split("/").some((seg) => seg === "" || seg === "." || seg === "..");
|
|
50878
|
+
if (bad)
|
|
50879
|
+
throw new TypeError(`buildZip: unsafe entry name ${JSON.stringify(name)}.`);
|
|
50880
|
+
if (seen.has(name))
|
|
50881
|
+
throw new TypeError(`buildZip: duplicate entry name ${JSON.stringify(name)}.`);
|
|
50882
|
+
seen.add(name);
|
|
50566
50883
|
}
|
|
50567
|
-
function
|
|
50568
|
-
|
|
50569
|
-
|
|
50570
|
-
|
|
50884
|
+
function buildZip(entries, modifiedAt = new Date(Date.UTC(1980, 0, 1))) {
|
|
50885
|
+
if (entries.length > MAX_ENTRIES)
|
|
50886
|
+
throw new TypeError(`buildZip: more than ${MAX_ENTRIES} entries needs ZIP64.`);
|
|
50887
|
+
const { time: time3, date: date5 } = dosDateTime(modifiedAt);
|
|
50888
|
+
const encoder = new TextEncoder();
|
|
50889
|
+
const seen = /* @__PURE__ */ new Set();
|
|
50890
|
+
const parts = [];
|
|
50891
|
+
const central = [];
|
|
50892
|
+
let offset = 0;
|
|
50893
|
+
for (const entry of entries) {
|
|
50894
|
+
checkName(entry.name, seen);
|
|
50895
|
+
const name = encoder.encode(entry.name);
|
|
50896
|
+
const size = entry.bytes.byteLength;
|
|
50897
|
+
const crc = crc32(entry.bytes);
|
|
50898
|
+
let method = METHOD_STORE;
|
|
50899
|
+
let data = entry.bytes;
|
|
50900
|
+
if (entry.compress !== false && size > 0) {
|
|
50901
|
+
const deflated = new Uint8Array(deflateRawSync(entry.bytes));
|
|
50902
|
+
if (deflated.byteLength < size) {
|
|
50903
|
+
method = METHOD_DEFLATE;
|
|
50904
|
+
data = deflated;
|
|
50905
|
+
}
|
|
50906
|
+
}
|
|
50907
|
+
const csize = data.byteLength;
|
|
50908
|
+
const local = new Uint8Array(30 + name.byteLength);
|
|
50909
|
+
const lv = new DataView(local.buffer);
|
|
50910
|
+
lv.setUint32(0, LOCAL_HEADER, true);
|
|
50911
|
+
lv.setUint16(4, VERSION2, true);
|
|
50912
|
+
lv.setUint16(6, FLAG_UTF8, true);
|
|
50913
|
+
lv.setUint16(8, method, true);
|
|
50914
|
+
lv.setUint16(10, time3, true);
|
|
50915
|
+
lv.setUint16(12, date5, true);
|
|
50916
|
+
lv.setUint32(14, crc, true);
|
|
50917
|
+
lv.setUint32(18, csize, true);
|
|
50918
|
+
lv.setUint32(22, size, true);
|
|
50919
|
+
lv.setUint16(26, name.byteLength, true);
|
|
50920
|
+
lv.setUint16(28, 0, true);
|
|
50921
|
+
local.set(name, 30);
|
|
50922
|
+
const dir = new Uint8Array(46 + name.byteLength);
|
|
50923
|
+
const dv = new DataView(dir.buffer);
|
|
50924
|
+
dv.setUint32(0, CENTRAL_HEADER, true);
|
|
50925
|
+
dv.setUint16(4, VERSION2, true);
|
|
50926
|
+
dv.setUint16(6, VERSION2, true);
|
|
50927
|
+
dv.setUint16(8, FLAG_UTF8, true);
|
|
50928
|
+
dv.setUint16(10, method, true);
|
|
50929
|
+
dv.setUint16(12, time3, true);
|
|
50930
|
+
dv.setUint16(14, date5, true);
|
|
50931
|
+
dv.setUint32(16, crc, true);
|
|
50932
|
+
dv.setUint32(20, csize, true);
|
|
50933
|
+
dv.setUint32(24, size, true);
|
|
50934
|
+
dv.setUint16(28, name.byteLength, true);
|
|
50935
|
+
dv.setUint32(42, offset, true);
|
|
50936
|
+
dir.set(name, 46);
|
|
50937
|
+
parts.push(local, data);
|
|
50938
|
+
central.push(dir);
|
|
50939
|
+
offset += local.byteLength + csize;
|
|
50940
|
+
if (offset > MAX_U32)
|
|
50941
|
+
throw new TypeError("buildZip: archive larger than 4 GiB needs ZIP64.");
|
|
50571
50942
|
}
|
|
50572
|
-
|
|
50573
|
-
|
|
50574
|
-
|
|
50575
|
-
const
|
|
50576
|
-
|
|
50577
|
-
|
|
50943
|
+
const centralSize = central.reduce((n, c) => n + c.byteLength, 0);
|
|
50944
|
+
if (offset + centralSize > MAX_U32)
|
|
50945
|
+
throw new TypeError("buildZip: archive larger than 4 GiB needs ZIP64.");
|
|
50946
|
+
const eocd = new Uint8Array(22);
|
|
50947
|
+
const ev = new DataView(eocd.buffer);
|
|
50948
|
+
ev.setUint32(0, EOCD, true);
|
|
50949
|
+
ev.setUint16(8, entries.length, true);
|
|
50950
|
+
ev.setUint16(10, entries.length, true);
|
|
50951
|
+
ev.setUint32(12, centralSize, true);
|
|
50952
|
+
ev.setUint32(16, offset, true);
|
|
50953
|
+
const out = new Uint8Array(offset + centralSize + eocd.byteLength);
|
|
50954
|
+
let cursor = 0;
|
|
50955
|
+
for (const part of [...parts, ...central, eocd]) {
|
|
50956
|
+
out.set(part, cursor);
|
|
50957
|
+
cursor += part.byteLength;
|
|
50578
50958
|
}
|
|
50579
50959
|
return out;
|
|
50580
50960
|
}
|
|
50581
|
-
function projectCollection(payload, project, label) {
|
|
50582
|
-
const items = Array.isArray(payload?.value) ? payload.value : [];
|
|
50583
|
-
return pruned({
|
|
50584
|
-
count: items.length,
|
|
50585
|
-
items: items.map(
|
|
50586
|
-
(item) => projectOrRaw(item, project, { label, context: "office-outlook-mcp" })
|
|
50587
|
-
),
|
|
50588
|
-
nextLink: payload?.["@odata.nextLink"]
|
|
50589
|
-
});
|
|
50590
|
-
}
|
|
50591
50961
|
|
|
50592
|
-
//
|
|
50593
|
-
var
|
|
50594
|
-
|
|
50595
|
-
|
|
50962
|
+
// node_modules/@chrischall/mcp-utils/dist/export/sink.js
|
|
50963
|
+
var EXPORT_MARKER = ".mcp-export";
|
|
50964
|
+
var EXPORT_DIR_NAME = /-\d{8}-\d{6}(?:-\d+)?$/;
|
|
50965
|
+
var EXPORT_BLOB_MAX_BYTES = 256 * 1024 * 1024;
|
|
50966
|
+
var EXPORT_ID = /^[A-Za-z0-9_-]{8,64}$/;
|
|
50967
|
+
var MAX_PART = 9999;
|
|
50968
|
+
var MAX_REL_PATH = 1024;
|
|
50969
|
+
var MAX_SEGMENT = 255;
|
|
50970
|
+
var CONTROL = /[\x00-\x1f\x7f]/;
|
|
50971
|
+
function badInput2(message, hint) {
|
|
50972
|
+
return new McpToolError(message, hint !== void 0 ? { hint, kind: "unknown" } : { kind: "unknown" });
|
|
50596
50973
|
}
|
|
50597
|
-
|
|
50598
|
-
|
|
50599
|
-
|
|
50600
|
-
|
|
50974
|
+
function exportAllowedRoots(envVar, env = process.env) {
|
|
50975
|
+
const envDir = readEnvVar(envVar, { env });
|
|
50976
|
+
if (envDir)
|
|
50977
|
+
return [expandPath(envDir)];
|
|
50978
|
+
const home = homedir5();
|
|
50979
|
+
return [join10(home, "Downloads"), join10(home, "Documents"), join10(home, "Desktop")];
|
|
50601
50980
|
}
|
|
50602
|
-
|
|
50603
|
-
|
|
50604
|
-
|
|
50605
|
-
|
|
50606
|
-
|
|
50607
|
-
|
|
50608
|
-
)
|
|
50609
|
-
function fetchPage(client2, nextLink, path, opts = {}) {
|
|
50610
|
-
if (nextLink === void 0) return client2.get(path, opts);
|
|
50611
|
-
const { query: _ignored, ...headerOpts } = opts;
|
|
50612
|
-
return client2.getAbsolute(nextLink, headerOpts);
|
|
50981
|
+
function exportPartName(part, ext) {
|
|
50982
|
+
if (!Number.isInteger(part) || part < 1 || part > MAX_PART) {
|
|
50983
|
+
throw new TypeError(`exportPartName: part must be an integer 1-${MAX_PART}, got ${part}.`);
|
|
50984
|
+
}
|
|
50985
|
+
if (!isSafeExtension(ext))
|
|
50986
|
+
throw new TypeError(`exportPartName: unsafe extension ${JSON.stringify(ext)}.`);
|
|
50987
|
+
return `part-${String(part).padStart(4, "0")}.${ext}`;
|
|
50613
50988
|
}
|
|
50614
|
-
function
|
|
50615
|
-
|
|
50616
|
-
|
|
50617
|
-
|
|
50618
|
-
if (
|
|
50619
|
-
|
|
50989
|
+
function sanitizeExportFileName(name, maxLength = 120) {
|
|
50990
|
+
let s = String(name).replace(/[\x00-\x1f\x7f/\\:*?"<>|]/g, "_").replace(/\.{2,}/g, "_").replace(/_{2,}/g, "_").replace(/^[\s.]+|[\s.]+$/g, "");
|
|
50991
|
+
if (!s || /^_+$/.test(s))
|
|
50992
|
+
return "file";
|
|
50993
|
+
if (s.length > maxLength) {
|
|
50994
|
+
const dot = s.lastIndexOf(".");
|
|
50995
|
+
const ext = dot > 0 && s.length - dot <= 17 ? s.slice(dot) : "";
|
|
50996
|
+
s = s.slice(0, maxLength - ext.length) + ext;
|
|
50997
|
+
}
|
|
50998
|
+
return s;
|
|
50620
50999
|
}
|
|
50621
|
-
|
|
50622
|
-
|
|
50623
|
-
)
|
|
50624
|
-
|
|
50625
|
-
|
|
50626
|
-
|
|
50627
|
-
|
|
50628
|
-
|
|
51000
|
+
function checkRelPath(relPath) {
|
|
51001
|
+
const fail = () => badInput2(`Unsafe export file name ${JSON.stringify(String(relPath).slice(0, 80))}.`, 'Use a relative, /-separated path with no empty, "." or ".." segment, backslash or control character (see sanitizeExportFileName).');
|
|
51002
|
+
if (typeof relPath !== "string" || relPath.length === 0 || relPath.length > MAX_REL_PATH)
|
|
51003
|
+
throw fail();
|
|
51004
|
+
if (relPath.includes("\\") || CONTROL.test(relPath))
|
|
51005
|
+
throw fail();
|
|
51006
|
+
const segs = relPath.split("/");
|
|
51007
|
+
for (const seg of segs) {
|
|
51008
|
+
if (seg === "" || seg === "." || seg === ".." || seg.length > MAX_SEGMENT)
|
|
51009
|
+
throw fail();
|
|
51010
|
+
}
|
|
51011
|
+
if (segs[0].toLowerCase() === EXPORT_MARKER)
|
|
51012
|
+
throw fail();
|
|
51013
|
+
return segs;
|
|
50629
51014
|
}
|
|
50630
|
-
|
|
50631
|
-
|
|
50632
|
-
|
|
50633
|
-
const ms = Date.parse(t);
|
|
50634
|
-
if (Number.isNaN(ms)) return void 0;
|
|
50635
|
-
if (new Date(ms + offsetMs(t)).toISOString().slice(0, 10) !== t.slice(0, 10)) return void 0;
|
|
50636
|
-
return ms;
|
|
51015
|
+
var COMPRESSIBLE_EXT = /\.(?:jsonl|ndjson|json|md|markdown|txt|csv|tsv|html?|xml|ics|vcf|eml|svg|ya?ml)$/i;
|
|
51016
|
+
function shouldCompress(name, contentType) {
|
|
51017
|
+
return contentType ? isCompressibleType(contentType) : COMPRESSIBLE_EXT.test(name);
|
|
50637
51018
|
}
|
|
50638
|
-
function
|
|
50639
|
-
const
|
|
50640
|
-
|
|
50641
|
-
const mins = Number(m[2]) * 60 + Number(m[3]);
|
|
50642
|
-
return (m[1] === "-" ? -1 : 1) * mins * 6e4;
|
|
51019
|
+
function stamp(ms) {
|
|
51020
|
+
const iso = new Date(ms).toISOString();
|
|
51021
|
+
return `${iso.slice(0, 4)}${iso.slice(5, 7)}${iso.slice(8, 10)}-${iso.slice(11, 13)}${iso.slice(14, 16)}${iso.slice(17, 19)}`;
|
|
50643
51022
|
}
|
|
50644
|
-
function
|
|
50645
|
-
|
|
51023
|
+
function checkPart(part) {
|
|
51024
|
+
if (!Number.isInteger(part) || part < 1 || part > MAX_PART) {
|
|
51025
|
+
throw badInput2(`Export part must be an integer from 1 to ${MAX_PART}.`, "Pass the cursor exactly as returned.");
|
|
51026
|
+
}
|
|
51027
|
+
return part;
|
|
50646
51028
|
}
|
|
50647
|
-
function
|
|
50648
|
-
if (
|
|
50649
|
-
|
|
50650
|
-
|
|
50651
|
-
|
|
50652
|
-
}
|
|
50653
|
-
return { t: utcLiteral(ms2), ids: [] };
|
|
51029
|
+
function checkExportId(id) {
|
|
51030
|
+
if (id === void 0)
|
|
51031
|
+
return randomBytes7(12).toString("base64url");
|
|
51032
|
+
if (typeof id !== "string" || !EXPORT_ID.test(id)) {
|
|
51033
|
+
throw badInput2("Export id is malformed.", "Pass the cursor exactly as returned, or start a new export without one.");
|
|
50654
51034
|
}
|
|
50655
|
-
|
|
50656
|
-
|
|
50657
|
-
|
|
50658
|
-
|
|
50659
|
-
|
|
51035
|
+
return id;
|
|
51036
|
+
}
|
|
51037
|
+
var toBytes = (data) => typeof data === "string" ? new TextEncoder().encode(data) : data;
|
|
51038
|
+
async function openExportSink(opts) {
|
|
51039
|
+
const env = opts.env ?? process.env;
|
|
51040
|
+
const part = checkPart(opts.part ?? 1);
|
|
51041
|
+
const exportId = checkExportId(opts.exportId);
|
|
51042
|
+
const now = opts.now;
|
|
51043
|
+
if (opts.maxBytes !== void 0 && !(Number.isSafeInteger(opts.maxBytes) && opts.maxBytes > 0)) {
|
|
51044
|
+
throw new TypeError(`openExportSink: maxBytes must be a positive whole number, got ${opts.maxBytes}.`);
|
|
50660
51045
|
}
|
|
50661
|
-
const
|
|
50662
|
-
|
|
50663
|
-
|
|
50664
|
-
|
|
51046
|
+
const store = blobStoreFromEnv(env);
|
|
51047
|
+
if (store)
|
|
51048
|
+
return openBlobSink(store, exportId, part, opts, now);
|
|
51049
|
+
if (opts.existingDir !== void 0) {
|
|
51050
|
+
return openDiskSink(resumeDir(opts.existingDir, opts.envVar, env, exportId, opts.name), exportId, part);
|
|
50665
51051
|
}
|
|
50666
|
-
|
|
50667
|
-
}
|
|
50668
|
-
|
|
50669
|
-
|
|
50670
|
-
}
|
|
50671
|
-
|
|
50672
|
-
const seen = new Set(pos.ids);
|
|
50673
|
-
const fresh = rows.filter((r) => r.Id === void 0 || !seen.has(r.Id));
|
|
50674
|
-
const kept = fresh.slice(0, limit);
|
|
50675
|
-
const next = nextPosition(kept, pos);
|
|
50676
|
-
return { rows: kept, next_from: encodeFrom(next), has_more: fresh.length > limit || apiHasMore };
|
|
51052
|
+
const dir = freshDir(opts, env, now);
|
|
51053
|
+
await writeFileSafe(join10(dir, EXPORT_MARKER), new TextEncoder().encode(JSON.stringify({ exportId, name: opts.name })), {
|
|
51054
|
+
allowedRoots: [dir],
|
|
51055
|
+
mode: 384
|
|
51056
|
+
});
|
|
51057
|
+
return openDiskSink(dir, exportId, part);
|
|
50677
51058
|
}
|
|
50678
|
-
function
|
|
50679
|
-
|
|
50680
|
-
if (dated.length === 0) return pos;
|
|
50681
|
-
const newest = Math.max(...dated.map((r) => r.ms));
|
|
50682
|
-
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)) : [];
|
|
50684
|
-
return { t: utcLiteral(newest), ids: [...carried, ...tied] };
|
|
51059
|
+
function outsideRoots(what, envVar) {
|
|
51060
|
+
return badInput2(`The ${what} is outside the directories exports may be written to.`, `Use a folder under ${envVar} if set, otherwise under ~/Downloads, ~/Documents or ~/Desktop.`);
|
|
50685
51061
|
}
|
|
50686
|
-
|
|
50687
|
-
|
|
50688
|
-
|
|
50689
|
-
|
|
50690
|
-
|
|
50691
|
-
|
|
50692
|
-
"deleteditems",
|
|
50693
|
-
"archive",
|
|
50694
|
-
"junkemail",
|
|
50695
|
-
"outbox",
|
|
50696
|
-
"clutter"
|
|
50697
|
-
];
|
|
50698
|
-
var LIST_SELECT = "Id,Subject,From,ToRecipients,ReceivedDateTime,IsRead,HasAttachments,BodyPreview";
|
|
50699
|
-
function folderSegment(folder) {
|
|
50700
|
-
return encodeURIComponent(folder);
|
|
51062
|
+
function within(p, roots, what, envVar) {
|
|
51063
|
+
try {
|
|
51064
|
+
return assertPathWithinRoots(p, roots);
|
|
51065
|
+
} catch {
|
|
51066
|
+
throw outsideRoots(what, envVar);
|
|
51067
|
+
}
|
|
50701
51068
|
}
|
|
50702
|
-
|
|
50703
|
-
|
|
50704
|
-
|
|
50705
|
-
|
|
50706
|
-
|
|
50707
|
-
|
|
50708
|
-
|
|
50709
|
-
|
|
50710
|
-
|
|
50711
|
-
"Categories",
|
|
50712
|
-
"Flag",
|
|
50713
|
-
"ConversationId",
|
|
50714
|
-
MEETING_MESSAGE_TYPE
|
|
50715
|
-
].join(",");
|
|
50716
|
-
var BODY_CONCURRENCY = 4;
|
|
50717
|
-
function triageKind(m) {
|
|
50718
|
-
switch (m.MeetingMessageType) {
|
|
50719
|
-
case "MeetingRequest":
|
|
50720
|
-
return "meetingRequest";
|
|
50721
|
-
case "MeetingCancelled":
|
|
50722
|
-
return "meetingCancelled";
|
|
50723
|
-
case "MeetingAccepted":
|
|
50724
|
-
case "MeetingTenativelyAccepted":
|
|
50725
|
-
// sic — Outlook's spelling
|
|
50726
|
-
case "MeetingTentativelyAccepted":
|
|
50727
|
-
case "MeetingDeclined":
|
|
50728
|
-
return "meetingResponse";
|
|
51069
|
+
function freshDir(opts, env, now) {
|
|
51070
|
+
const roots = exportAllowedRoots(opts.envVar, env);
|
|
51071
|
+
let parent;
|
|
51072
|
+
try {
|
|
51073
|
+
parent = resolveOutputDir(opts.dest, opts.envVar, { env, name: opts.name, allowedRoots: roots });
|
|
51074
|
+
} catch (err) {
|
|
51075
|
+
if (err instanceof McpToolError || err instanceof TypeError)
|
|
51076
|
+
throw err;
|
|
51077
|
+
throw outsideRoots("destination", opts.envVar);
|
|
50729
51078
|
}
|
|
50730
|
-
const
|
|
50731
|
-
|
|
50732
|
-
|
|
50733
|
-
|
|
50734
|
-
|
|
51079
|
+
const realParent = assertPathWithinRoots(parent, [parent]);
|
|
51080
|
+
const stem = `${sanitizeExportFileName(sanitizeBaseName(opts.baseName), 80)}-${stamp(now ?? Date.now())}`;
|
|
51081
|
+
for (let n = 1; n <= 1e3; n++) {
|
|
51082
|
+
const candidate2 = join10(realParent, n === 1 ? stem : `${stem}-${n}`);
|
|
51083
|
+
try {
|
|
51084
|
+
mkdirSync6(candidate2, { mode: 448 });
|
|
51085
|
+
return candidate2;
|
|
51086
|
+
} catch (err) {
|
|
51087
|
+
if (err.code !== "EEXIST")
|
|
51088
|
+
throw err;
|
|
51089
|
+
}
|
|
51090
|
+
}
|
|
51091
|
+
throw badInput2("Could not create a fresh export directory (1000 names taken).");
|
|
50735
51092
|
}
|
|
50736
|
-
function
|
|
50737
|
-
|
|
50738
|
-
const sender = who?.EmailAddress?.Name || who?.EmailAddress?.Address;
|
|
50739
|
-
const kind = triageKind(m);
|
|
50740
|
-
const title = sender ? `${sender}: ${m.Subject ?? "(no subject)"}` : m.Subject;
|
|
50741
|
-
return briefLine(title, briefDate(m.ReceivedDateTime), kind === "mail" ? "unread" : spaced(kind));
|
|
51093
|
+
function notThisExport() {
|
|
51094
|
+
return badInput2("The export directory to resume was not created by this export.", "Pass the cursor exactly as returned, or start a new export without one.");
|
|
50742
51095
|
}
|
|
50743
|
-
|
|
50744
|
-
|
|
50745
|
-
|
|
50746
|
-
|
|
50747
|
-
|
|
50748
|
-
|
|
50749
|
-
|
|
50750
|
-
|
|
50751
|
-
|
|
50752
|
-
|
|
50753
|
-
|
|
50754
|
-
|
|
51096
|
+
var MAX_MARKER = 4096;
|
|
51097
|
+
function readMarker(dir) {
|
|
51098
|
+
let fd;
|
|
51099
|
+
try {
|
|
51100
|
+
fd = openSync4(join10(dir, EXPORT_MARKER), fsConstants3.O_RDONLY | (fsConstants3.O_NOFOLLOW ?? 0));
|
|
51101
|
+
} catch {
|
|
51102
|
+
return void 0;
|
|
51103
|
+
}
|
|
51104
|
+
try {
|
|
51105
|
+
const buf = Buffer.alloc(MAX_MARKER);
|
|
51106
|
+
const n = readSync(fd, buf, 0, buf.length, 0);
|
|
51107
|
+
const parsed = JSON.parse(buf.subarray(0, n).toString("utf8"));
|
|
51108
|
+
const id = parsed?.exportId;
|
|
51109
|
+
const name = parsed?.name;
|
|
51110
|
+
if (typeof id !== "string")
|
|
51111
|
+
return void 0;
|
|
51112
|
+
return { exportId: id, name: typeof name === "string" ? name : void 0 };
|
|
51113
|
+
} catch {
|
|
51114
|
+
return void 0;
|
|
51115
|
+
} finally {
|
|
51116
|
+
closeSync4(fd);
|
|
51117
|
+
}
|
|
50755
51118
|
}
|
|
50756
|
-
|
|
50757
|
-
|
|
50758
|
-
|
|
50759
|
-
|
|
50760
|
-
|
|
50761
|
-
|
|
50762
|
-
|
|
51119
|
+
function resumeDir(existingDir, envVar, env, exportId, name) {
|
|
51120
|
+
if (typeof existingDir !== "string" || existingDir.length === 0)
|
|
51121
|
+
throw outsideRoots("export directory", envVar);
|
|
51122
|
+
const roots = exportAllowedRoots(envVar, env);
|
|
51123
|
+
const real = within(existingDir, roots, "export directory", envVar);
|
|
51124
|
+
for (const root of roots) {
|
|
51125
|
+
if (real === within(root, [root], "export directory", envVar))
|
|
51126
|
+
throw outsideRoots("export directory", envVar);
|
|
51127
|
+
}
|
|
51128
|
+
let isDir = false;
|
|
51129
|
+
try {
|
|
51130
|
+
isDir = lstatSync2(real).isDirectory();
|
|
51131
|
+
} catch {
|
|
51132
|
+
isDir = false;
|
|
51133
|
+
}
|
|
51134
|
+
if (!isDir) {
|
|
51135
|
+
throw badInput2("The export directory to resume no longer exists.", "Start a new export without a cursor.");
|
|
51136
|
+
}
|
|
51137
|
+
if (!EXPORT_DIR_NAME.test(basename2(real)))
|
|
51138
|
+
throw notThisExport();
|
|
51139
|
+
let markerIsFile = false;
|
|
51140
|
+
try {
|
|
51141
|
+
markerIsFile = lstatSync2(join10(real, EXPORT_MARKER)).isFile();
|
|
51142
|
+
} catch {
|
|
51143
|
+
markerIsFile = false;
|
|
51144
|
+
}
|
|
51145
|
+
if (!markerIsFile)
|
|
51146
|
+
throw notThisExport();
|
|
51147
|
+
const marker = readMarker(real);
|
|
51148
|
+
if (!marker || marker.exportId !== exportId || marker.name !== name)
|
|
51149
|
+
throw notThisExport();
|
|
51150
|
+
return real;
|
|
50763
51151
|
}
|
|
50764
|
-
|
|
50765
|
-
|
|
50766
|
-
|
|
50767
|
-
|
|
50768
|
-
|
|
50769
|
-
|
|
50770
|
-
|
|
50771
|
-
|
|
50772
|
-
|
|
50773
|
-
|
|
51152
|
+
function openDiskSink(dir, exportId, part) {
|
|
51153
|
+
const files = /* @__PURE__ */ new Map();
|
|
51154
|
+
let finished = false;
|
|
51155
|
+
return {
|
|
51156
|
+
mode: "disk",
|
|
51157
|
+
exportId,
|
|
51158
|
+
part,
|
|
51159
|
+
dir,
|
|
51160
|
+
async addFile(relPath, data, _contentType, o = {}) {
|
|
51161
|
+
if (finished)
|
|
51162
|
+
throw badInput2("This export part is already finished.");
|
|
51163
|
+
const segs = checkRelPath(relPath);
|
|
51164
|
+
if (files.has(relPath) && !o.overwrite)
|
|
51165
|
+
throw badInput2(`Export file ${relPath} was already written.`);
|
|
51166
|
+
const target = join10(dir, ...segs);
|
|
51167
|
+
const parent = within(dirname5(target), [dir], "export file", "the export");
|
|
51168
|
+
await mkdir4(parent, { recursive: true, mode: 448 });
|
|
51169
|
+
const bytes = toBytes(data);
|
|
51170
|
+
const path = await writeFileSafe(target, bytes, {
|
|
51171
|
+
allowedRoots: [dir],
|
|
51172
|
+
mode: 384,
|
|
51173
|
+
...o.overwrite ? { overwrite: true } : {}
|
|
51174
|
+
});
|
|
51175
|
+
const rec = { name: relPath, path, bytes: bytes.byteLength };
|
|
51176
|
+
files.delete(relPath);
|
|
51177
|
+
files.set(relPath, rec);
|
|
51178
|
+
return rec;
|
|
51179
|
+
},
|
|
51180
|
+
async finish() {
|
|
51181
|
+
if (finished)
|
|
51182
|
+
throw badInput2("This export part is already finished.");
|
|
51183
|
+
finished = true;
|
|
51184
|
+
const list = [...files.values()];
|
|
51185
|
+
return {
|
|
51186
|
+
mode: "disk",
|
|
51187
|
+
exportId,
|
|
51188
|
+
part,
|
|
51189
|
+
location: dir,
|
|
51190
|
+
dir,
|
|
51191
|
+
bytes: list.reduce((n, f) => n + f.bytes, 0),
|
|
51192
|
+
files: list
|
|
51193
|
+
};
|
|
51194
|
+
}
|
|
51195
|
+
};
|
|
50774
51196
|
}
|
|
50775
|
-
function
|
|
50776
|
-
|
|
51197
|
+
function openBlobSink(store, exportId, part, opts, now) {
|
|
51198
|
+
const key = `exp/${exportId}/${exportPartName(part, "zip")}`;
|
|
51199
|
+
const signOpts = { ...opts.ttlMs !== void 0 ? { ttlMs: opts.ttlMs } : {} };
|
|
51200
|
+
store.signGetUrl(key, { ...signOpts, now: now ?? Date.now() });
|
|
51201
|
+
const entries = /* @__PURE__ */ new Map();
|
|
51202
|
+
const maxBytes = opts.maxBytes ?? EXPORT_BLOB_MAX_BYTES;
|
|
51203
|
+
let held = 0;
|
|
51204
|
+
let finished = false;
|
|
51205
|
+
return {
|
|
51206
|
+
mode: "blob",
|
|
51207
|
+
exportId,
|
|
51208
|
+
part,
|
|
51209
|
+
async addFile(relPath, data, contentType, o = {}) {
|
|
51210
|
+
if (finished)
|
|
51211
|
+
throw badInput2("This export part is already finished.");
|
|
51212
|
+
checkRelPath(relPath);
|
|
51213
|
+
if (entries.has(relPath) && !o.overwrite)
|
|
51214
|
+
throw badInput2(`Export file ${relPath} was already written.`);
|
|
51215
|
+
const bytes = toBytes(data);
|
|
51216
|
+
const replaced = entries.get(relPath)?.bytes.byteLength ?? 0;
|
|
51217
|
+
if (bytes.byteLength > maxBytes) {
|
|
51218
|
+
throw new McpToolError(`Export file ${relPath} alone is ${bytes.byteLength} bytes, over the ${maxBytes}-byte part limit.`, {
|
|
51219
|
+
kind: "too_large",
|
|
51220
|
+
hint: "No export part can hold this file: skip it, or raise the part limit (maxBytes)."
|
|
51221
|
+
});
|
|
51222
|
+
}
|
|
51223
|
+
if (held - replaced + bytes.byteLength > maxBytes) {
|
|
51224
|
+
throw new McpToolError(`This export part would exceed its ${maxBytes}-byte limit with ${relPath}.`, {
|
|
51225
|
+
kind: "too_large",
|
|
51226
|
+
hint: "Finish this part and continue the export in the next one (return the resume cursor)."
|
|
51227
|
+
});
|
|
51228
|
+
}
|
|
51229
|
+
held += bytes.byteLength - replaced;
|
|
51230
|
+
entries.delete(relPath);
|
|
51231
|
+
entries.set(relPath, { bytes, compress: shouldCompress(relPath, contentType) });
|
|
51232
|
+
return { name: relPath, bytes: bytes.byteLength };
|
|
51233
|
+
},
|
|
51234
|
+
async finish() {
|
|
51235
|
+
if (finished)
|
|
51236
|
+
throw badInput2("This export part is already finished.");
|
|
51237
|
+
finished = true;
|
|
51238
|
+
const at = now ?? Date.now();
|
|
51239
|
+
const zip = buildZip([...entries].map(([name, e]) => ({ name, bytes: e.bytes, compress: e.compress })), new Date(at));
|
|
51240
|
+
await store.put(key, zip, "application/zip", {
|
|
51241
|
+
...signOpts,
|
|
51242
|
+
now: at,
|
|
51243
|
+
...opts.fetchImpl ? { fetchImpl: opts.fetchImpl } : {},
|
|
51244
|
+
...opts.signal ? { signal: opts.signal } : {}
|
|
51245
|
+
});
|
|
51246
|
+
const { url: url2, expiresAtMs } = store.signGetUrl(key, { ...signOpts, now: at });
|
|
51247
|
+
const files = [...entries].map(([name, e]) => ({ name, bytes: e.bytes.byteLength }));
|
|
51248
|
+
return {
|
|
51249
|
+
mode: "blob",
|
|
51250
|
+
exportId,
|
|
51251
|
+
part,
|
|
51252
|
+
location: url2,
|
|
51253
|
+
key,
|
|
51254
|
+
urlExpiresAt: new Date(expiresAtMs).toISOString(),
|
|
51255
|
+
archiveBytes: zip.byteLength,
|
|
51256
|
+
bytes: files.reduce((n, f) => n + f.bytes, 0),
|
|
51257
|
+
files
|
|
51258
|
+
};
|
|
51259
|
+
}
|
|
51260
|
+
};
|
|
50777
51261
|
}
|
|
50778
|
-
|
|
50779
|
-
|
|
51262
|
+
var EXPORT_CURSOR_MAX_LENGTH = 16384;
|
|
51263
|
+
var CURSOR_VERSION = 1;
|
|
51264
|
+
function encodeExportCursor(state) {
|
|
51265
|
+
const out = Buffer.from(JSON.stringify({ v: CURSOR_VERSION, ...state }), "utf8").toString("base64url");
|
|
51266
|
+
if (out.length > EXPORT_CURSOR_MAX_LENGTH) {
|
|
51267
|
+
throw new TypeError(`encodeExportCursor: cursor is ${out.length} characters (max ${EXPORT_CURSOR_MAX_LENGTH}).`);
|
|
51268
|
+
}
|
|
51269
|
+
return out;
|
|
50780
51270
|
}
|
|
50781
|
-
function
|
|
50782
|
-
const
|
|
50783
|
-
|
|
51271
|
+
function decodeExportCursor(cursor) {
|
|
51272
|
+
const bad = () => badInput2("The export cursor is not valid.", "Pass the cursor exactly as the previous call returned it, or start a new export without one.");
|
|
51273
|
+
if (typeof cursor !== "string" || cursor.length === 0 || cursor.length > EXPORT_CURSOR_MAX_LENGTH)
|
|
51274
|
+
throw bad();
|
|
51275
|
+
if (!/^[A-Za-z0-9_-]+$/.test(cursor))
|
|
51276
|
+
throw bad();
|
|
51277
|
+
let parsed;
|
|
51278
|
+
try {
|
|
51279
|
+
parsed = JSON.parse(Buffer.from(cursor, "base64url").toString("utf8"));
|
|
51280
|
+
} catch {
|
|
51281
|
+
throw bad();
|
|
51282
|
+
}
|
|
51283
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed))
|
|
51284
|
+
throw bad();
|
|
51285
|
+
const obj = parsed;
|
|
51286
|
+
if (obj.v !== CURSOR_VERSION)
|
|
51287
|
+
throw bad();
|
|
51288
|
+
return obj;
|
|
50784
51289
|
}
|
|
50785
|
-
function
|
|
50786
|
-
|
|
50787
|
-
|
|
50788
|
-
|
|
50789
|
-
return
|
|
51290
|
+
function jsonlLine(value) {
|
|
51291
|
+
const s = JSON.stringify(value);
|
|
51292
|
+
if (s === void 0)
|
|
51293
|
+
throw new TypeError("jsonlLine: value has no JSON representation.");
|
|
51294
|
+
return `${s}
|
|
51295
|
+
`;
|
|
50790
51296
|
}
|
|
50791
|
-
|
|
50792
|
-
|
|
51297
|
+
|
|
51298
|
+
// node_modules/@chrischall/mcp-utils/dist/export/upload.js
|
|
51299
|
+
init_zod();
|
|
51300
|
+
var UPLOAD_SLOT_DEFAULT_TTL_MS = 30 * 60 * 1e3;
|
|
51301
|
+
var MIB = 1024 * 1024;
|
|
51302
|
+
var OCTET_STREAM = "application/octet-stream";
|
|
51303
|
+
var inlineFileSchema = external_exports.object({
|
|
51304
|
+
filename: external_exports.string().describe("Filename the upstream will show \u2014 a bare name, no path, not starting with a dot. Its extension sets the MIME type when mimeType is omitted."),
|
|
51305
|
+
contentBase64: external_exports.string().describe(`The file's bytes, standard base64 (line breaks and a "data:<type>;base64," prefix are tolerated).`),
|
|
51306
|
+
mimeType: external_exports.string().describe('MIME type, e.g. "image/jpeg". Default: inferred from the filename extension.').optional()
|
|
51307
|
+
});
|
|
51308
|
+
var uploadSlotRequestSchema = external_exports.object({
|
|
51309
|
+
filename: external_exports.string().describe("Filename the upstream will show \u2014 a bare name, no path, not starting with a dot."),
|
|
51310
|
+
mimeType: external_exports.string().describe("MIME type. Default: inferred from the filename extension. The PUT must send exactly this Content-Type.").optional()
|
|
51311
|
+
});
|
|
51312
|
+
var MIME_BY_EXT = {
|
|
51313
|
+
pdf: "application/pdf",
|
|
51314
|
+
png: "image/png",
|
|
51315
|
+
jpg: "image/jpeg",
|
|
51316
|
+
jpeg: "image/jpeg",
|
|
51317
|
+
gif: "image/gif",
|
|
51318
|
+
webp: "image/webp",
|
|
51319
|
+
heic: "image/heic",
|
|
51320
|
+
heif: "image/heif",
|
|
51321
|
+
tif: "image/tiff",
|
|
51322
|
+
tiff: "image/tiff",
|
|
51323
|
+
svg: "image/svg+xml",
|
|
51324
|
+
txt: "text/plain",
|
|
51325
|
+
md: "text/markdown",
|
|
51326
|
+
csv: "text/csv",
|
|
51327
|
+
html: "text/html",
|
|
51328
|
+
htm: "text/html",
|
|
51329
|
+
json: "application/json",
|
|
51330
|
+
xml: "application/xml",
|
|
51331
|
+
ics: "text/calendar",
|
|
51332
|
+
doc: "application/msword",
|
|
51333
|
+
docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
51334
|
+
xls: "application/vnd.ms-excel",
|
|
51335
|
+
xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
51336
|
+
ppt: "application/vnd.ms-powerpoint",
|
|
51337
|
+
pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
51338
|
+
zip: "application/zip",
|
|
51339
|
+
mp3: "audio/mpeg",
|
|
51340
|
+
m4a: "audio/mp4",
|
|
51341
|
+
wav: "audio/wav",
|
|
51342
|
+
mp4: "video/mp4",
|
|
51343
|
+
mov: "video/quicktime",
|
|
51344
|
+
webm: "video/webm"
|
|
51345
|
+
};
|
|
51346
|
+
function mimeFromFileName(name) {
|
|
51347
|
+
const dot = name.lastIndexOf(".");
|
|
51348
|
+
if (dot <= 0)
|
|
51349
|
+
return OCTET_STREAM;
|
|
51350
|
+
return MIME_BY_EXT[name.slice(dot + 1).toLowerCase()] ?? OCTET_STREAM;
|
|
50793
51351
|
}
|
|
50794
|
-
|
|
50795
|
-
|
|
50796
|
-
|
|
50797
|
-
|
|
50798
|
-
|
|
50799
|
-
|
|
50800
|
-
|
|
50801
|
-
}
|
|
50802
|
-
return decodeFrom(from);
|
|
51352
|
+
|
|
51353
|
+
// node_modules/@chrischall/mcp-utils/dist/export/upload-source.js
|
|
51354
|
+
init_config();
|
|
51355
|
+
init_errors3();
|
|
51356
|
+
init_magic();
|
|
51357
|
+
init_upload_guard();
|
|
51358
|
+
function isHostedEnv(env = process.env) {
|
|
51359
|
+
return readEnvVar("MCP_DATA_DIR", { env }) !== void 0 || readEnvVar("MCP_BLOB_BASE_URL", { env }) !== void 0;
|
|
50803
51360
|
}
|
|
50804
|
-
|
|
50805
|
-
|
|
50806
|
-
|
|
50807
|
-
|
|
50808
|
-
|
|
50809
|
-
|
|
50810
|
-
|
|
50811
|
-
|
|
50812
|
-
|
|
50813
|
-
|
|
50814
|
-
|
|
50815
|
-
|
|
50816
|
-
|
|
50817
|
-
|
|
50818
|
-
|
|
50819
|
-
const
|
|
50820
|
-
|
|
50821
|
-
|
|
50822
|
-
const
|
|
50823
|
-
|
|
51361
|
+
|
|
51362
|
+
// node_modules/@chrischall/mcp-utils/dist/export/deliver.js
|
|
51363
|
+
init_errors3();
|
|
51364
|
+
init_magic();
|
|
51365
|
+
init_output();
|
|
51366
|
+
init_write();
|
|
51367
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
51368
|
+
var INLINE_DELIVERY_MAX_BYTES = 10 * 1024 * 1024;
|
|
51369
|
+
var DELIVERY_LINK_DEFAULT_TTL_MS = 60 * 60 * 1e3;
|
|
51370
|
+
var RENDERABLE_IMAGES = /* @__PURE__ */ new Set(["image/png", "image/jpeg", "image/gif", "image/webp"]);
|
|
51371
|
+
function bareMime(raw) {
|
|
51372
|
+
const bare = raw?.split(";", 1)[0]?.trim().toLowerCase();
|
|
51373
|
+
return bare ? bare : void 0;
|
|
51374
|
+
}
|
|
51375
|
+
function resolveMime(bytes, declared, fileName) {
|
|
51376
|
+
const sniffed = sniffMimeBytes(bytes);
|
|
51377
|
+
if (sniffed !== void 0 && RENDERABLE_IMAGES.has(sniffed))
|
|
51378
|
+
return sniffed;
|
|
51379
|
+
const bare = bareMime(declared);
|
|
51380
|
+
if (bare !== void 0 && bare !== "application/octet-stream")
|
|
51381
|
+
return bare;
|
|
51382
|
+
return sniffed ?? mimeFromFileName(fileName);
|
|
51383
|
+
}
|
|
51384
|
+
function keySegment(fileName) {
|
|
51385
|
+
const seg = fileName.replace(/[^A-Za-z0-9._~-]/g, "_").replace(/^\.+/, "");
|
|
51386
|
+
return /^[._~-]*$/.test(seg) ? "file" : seg;
|
|
51387
|
+
}
|
|
51388
|
+
async function deliverFile(opts) {
|
|
51389
|
+
const { bytes, fileName } = opts;
|
|
51390
|
+
const store = opts.store === void 0 ? blobStoreFromEnv() : opts.store ?? void 0;
|
|
51391
|
+
const inlineMax = opts.inlineMaxBytes ?? INLINE_DELIVERY_MAX_BYTES;
|
|
51392
|
+
const mimeType = resolveMime(bytes, opts.mimeType, fileName);
|
|
51393
|
+
const base = { fileName, mimeType, sizeBytes: bytes.length };
|
|
51394
|
+
const fits = bytes.length <= inlineMax;
|
|
51395
|
+
const tooBig = `${fileName} is ${bytes.length} bytes, too large to return inline (limit ${inlineMax}).`;
|
|
51396
|
+
const inline = (note) => {
|
|
51397
|
+
const data = Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength).toString("base64");
|
|
51398
|
+
const content = RENDERABLE_IMAGES.has(mimeType) ? [{ type: "image", data, mimeType }] : [{ type: "resource", resource: { uri: `attachment:///${encodeURIComponent(fileName)}`, mimeType, blob: data } }];
|
|
51399
|
+
return { via: RENDERABLE_IMAGES.has(mimeType) ? "image" : "resource", ...base, content, ...note ? { note } : {} };
|
|
51400
|
+
};
|
|
51401
|
+
const link2 = async (note) => {
|
|
51402
|
+
const key = `${opts.keyPrefix ?? "files"}/${randomUUID2()}/${keySegment(fileName)}`;
|
|
51403
|
+
await store.put(key, bytes, mimeType);
|
|
51404
|
+
const signed = store.signGetUrl(key, { ttlMs: opts.linkTtlMs ?? DELIVERY_LINK_DEFAULT_TTL_MS });
|
|
51405
|
+
return {
|
|
51406
|
+
via: "link",
|
|
51407
|
+
...base,
|
|
51408
|
+
content: [],
|
|
51409
|
+
url: signed.url,
|
|
51410
|
+
expiresAt: new Date(signed.expiresAtMs).toISOString(),
|
|
51411
|
+
...note ? { note } : {}
|
|
51412
|
+
};
|
|
51413
|
+
};
|
|
51414
|
+
const disk = async (note) => {
|
|
51415
|
+
const dot = fileName.lastIndexOf(".");
|
|
51416
|
+
const ownExt = dot > 0 ? fileName.slice(dot + 1).toLowerCase() : "";
|
|
51417
|
+
const extension2 = ownExt && isSafeExtension(ownExt) ? ownExt : extensionForMime(mimeType);
|
|
51418
|
+
const stem = dot > 0 && ownExt === extension2 ? fileName.slice(0, dot) : fileName;
|
|
51419
|
+
const path = await writeUniqueFile({
|
|
51420
|
+
dir: opts.disk.dir,
|
|
51421
|
+
baseName: stem,
|
|
51422
|
+
extension: extension2,
|
|
51423
|
+
bytes,
|
|
51424
|
+
...opts.disk.allowedRoots ? { allowedRoots: opts.disk.allowedRoots } : {},
|
|
51425
|
+
...opts.disk.mode !== void 0 ? { mode: opts.disk.mode } : {}
|
|
51426
|
+
});
|
|
51427
|
+
return { via: "disk", ...base, content: [], path, ...note ? { note } : {} };
|
|
51428
|
+
};
|
|
51429
|
+
const noRoute = () => {
|
|
51430
|
+
throw new McpToolError(`${fileName} is ${bytes.length} bytes, over the ${inlineMax}-byte inline limit, and this server has no other way to hand it over (no blob store for a download link, no download directory).`, { kind: "too_large", hint: "Open the file in the service's own web app, or run this server where it has a download directory or a blob store." });
|
|
51431
|
+
};
|
|
51432
|
+
const prefer = opts.prefer ?? "auto";
|
|
51433
|
+
if (prefer === "link") {
|
|
51434
|
+
if (store)
|
|
51435
|
+
return link2();
|
|
51436
|
+
if (opts.disk)
|
|
51437
|
+
return disk("There is no blob store for a download link here, so the file was saved to disk instead.");
|
|
51438
|
+
if (fits)
|
|
51439
|
+
return inline("There is no blob store for a download link here, so the file is returned inline instead.");
|
|
51440
|
+
return noRoute();
|
|
51441
|
+
}
|
|
51442
|
+
if (prefer === "disk") {
|
|
51443
|
+
if (store)
|
|
51444
|
+
return link2("This server is hosted and the server's disk is not reachable from your session, so a download link is returned instead of a saved file.");
|
|
51445
|
+
if (opts.disk)
|
|
51446
|
+
return disk();
|
|
51447
|
+
if (fits)
|
|
51448
|
+
return inline("This server has no download directory configured, so the file is returned inline instead.");
|
|
51449
|
+
return noRoute();
|
|
51450
|
+
}
|
|
51451
|
+
if (prefer === "auto" && store && !RENDERABLE_IMAGES.has(mimeType)) {
|
|
51452
|
+
return link2(fits ? `Hosted clients only render images inline, so ${fileName} (${mimeType}) is delivered as a download link.` : `${tooBig} A download link is returned instead.`);
|
|
51453
|
+
}
|
|
51454
|
+
if (fits)
|
|
51455
|
+
return inline();
|
|
51456
|
+
if (store)
|
|
51457
|
+
return link2(`${tooBig} A download link is returned instead.`);
|
|
51458
|
+
if (opts.disk)
|
|
51459
|
+
return disk(`${tooBig} It was saved to disk instead.`);
|
|
51460
|
+
return noRoute();
|
|
50824
51461
|
}
|
|
50825
|
-
|
|
50826
|
-
|
|
50827
|
-
|
|
50828
|
-
|
|
50829
|
-
|
|
50830
|
-
|
|
50831
|
-
|
|
50832
|
-
|
|
50833
|
-
|
|
51462
|
+
|
|
51463
|
+
// src/tools/_hints.ts
|
|
51464
|
+
init_zod();
|
|
51465
|
+
init_dist2();
|
|
51466
|
+
|
|
51467
|
+
// src/tools/_untrusted.ts
|
|
51468
|
+
init_dist2();
|
|
51469
|
+
init_dist2();
|
|
51470
|
+
var UNTRUSTED_CONTENT_NOTE2 = "Message bodies, previews, subjects, names and event text below are written by other people (anyone who can email or invite this mailbox). " + UNTRUSTED_CONTENT_RULE;
|
|
51471
|
+
function mailboxUntrusted(payload) {
|
|
51472
|
+
return untrustedResult(payload, { note: UNTRUSTED_CONTENT_NOTE2 });
|
|
51473
|
+
}
|
|
51474
|
+
|
|
51475
|
+
// src/tools/_hints.ts
|
|
51476
|
+
function workMeta(intent, extra = {}) {
|
|
51477
|
+
return { mint: { domain: "work", intent, ...extra } };
|
|
51478
|
+
}
|
|
51479
|
+
var BRIEF_MAX = 120;
|
|
51480
|
+
var BRIEF_NOTE = '"brief": one line per item, `title \u2014 <ISO date or "no date"> \u2014 <status>`, at most 120 chars each, as `data` in the untrusted-content envelope; a line starting `-- ` is a note (`-- none` for an empty page; a title starting `-- ` or `\\` gets one leading `\\`); a last line `-- more` means another page exists (re-ask without "brief" for its nextLink).';
|
|
51481
|
+
function briefViewParam(rungs, note) {
|
|
51482
|
+
const base = viewParam(rungs);
|
|
51483
|
+
return external_exports.enum([...rungs, "brief"]).optional().describe(`${base.description ?? ""} ${BRIEF_NOTE} ${note}`.trim());
|
|
51484
|
+
}
|
|
51485
|
+
function flat(s) {
|
|
51486
|
+
const out = s?.replace(/\s+/g, " ").trim();
|
|
51487
|
+
return out ? out : void 0;
|
|
51488
|
+
}
|
|
51489
|
+
function briefDate(dt) {
|
|
51490
|
+
return flat(dt?.replace(/\.0+(?=Z?$)/, "")) ?? "no date";
|
|
51491
|
+
}
|
|
51492
|
+
var TITLE_FLOOR = 24;
|
|
51493
|
+
var cut = (s, n) => s.length > n ? `${s.slice(0, Math.max(n - 1, 0))}\u2026` : s;
|
|
51494
|
+
function briefLine(title, date5, status) {
|
|
51495
|
+
const raw = flat(title) ?? "(no subject)";
|
|
51496
|
+
const shown = raw.startsWith("-- ") || raw.startsWith("\\") ? `\\${raw}` : raw;
|
|
51497
|
+
const sep2 = ` \u2014 ${date5} \u2014 `;
|
|
51498
|
+
const room = BRIEF_MAX - sep2.length;
|
|
51499
|
+
if (shown.length + status.length <= room) return `${shown}${sep2}${status}`;
|
|
51500
|
+
const titleRoom = Math.max(room - status.length, Math.min(shown.length, TITLE_FLOOR));
|
|
51501
|
+
const t = cut(shown, titleRoom);
|
|
51502
|
+
return cut(`${t}${sep2}${cut(status, room - t.length)}`, BRIEF_MAX);
|
|
51503
|
+
}
|
|
51504
|
+
var BRIEF_MORE = "-- more";
|
|
51505
|
+
var BRIEF_NONE = "-- none";
|
|
51506
|
+
function briefResult(lines, hasMore = false) {
|
|
51507
|
+
const items = lines.length > 0 ? lines : [BRIEF_NONE];
|
|
51508
|
+
return mailboxUntrusted(hasMore ? [...items, BRIEF_MORE] : items);
|
|
51509
|
+
}
|
|
51510
|
+
function spaced(value) {
|
|
51511
|
+
return value.replace(/([a-z])([A-Z])/g, "$1 $2").toLowerCase();
|
|
51512
|
+
}
|
|
51513
|
+
|
|
51514
|
+
// src/view.ts
|
|
51515
|
+
init_dist2();
|
|
51516
|
+
var VIEWS2 = ["compact", "full", "raw"];
|
|
51517
|
+
function addr(r) {
|
|
51518
|
+
const e = r?.EmailAddress;
|
|
51519
|
+
if (!e) return void 0;
|
|
51520
|
+
if (e.Name && e.Address && e.Name !== e.Address) return `${e.Name} <${e.Address}>`;
|
|
51521
|
+
return e.Address ?? e.Name;
|
|
51522
|
+
}
|
|
51523
|
+
function addrs(list) {
|
|
51524
|
+
if (!Array.isArray(list) || list.length === 0) return void 0;
|
|
51525
|
+
return list.map((r) => addr(r)).filter((s) => s !== void 0);
|
|
51526
|
+
}
|
|
51527
|
+
var MAX_RECIPIENTS = 20;
|
|
51528
|
+
function cappedRecipients(key, list) {
|
|
51529
|
+
if (list === void 0 || list.length <= MAX_RECIPIENTS) return { [key]: list };
|
|
51530
|
+
return {
|
|
51531
|
+
[key]: list.slice(0, MAX_RECIPIENTS),
|
|
51532
|
+
[`${key}Count`]: list.length,
|
|
51533
|
+
[`${key}Truncated`]: true
|
|
51534
|
+
};
|
|
51535
|
+
}
|
|
51536
|
+
function compactMessage(m) {
|
|
51537
|
+
return pruned({
|
|
51538
|
+
Id: m.Id,
|
|
51539
|
+
Subject: m.Subject,
|
|
51540
|
+
From: addr(m.From ?? m.Sender),
|
|
51541
|
+
...cappedRecipients("To", addrs(m.ToRecipients)),
|
|
51542
|
+
Received: m.ReceivedDateTime,
|
|
51543
|
+
IsRead: m.IsRead,
|
|
51544
|
+
HasAttachments: m.HasAttachments || void 0,
|
|
51545
|
+
Preview: m.BodyPreview?.trim() || void 0,
|
|
51546
|
+
// Present only when the caller asked for a single message; a listing
|
|
51547
|
+
// projection would otherwise carry every body.
|
|
51548
|
+
Body: m.Body?.Content
|
|
51549
|
+
});
|
|
51550
|
+
}
|
|
51551
|
+
function fullMessage(m) {
|
|
51552
|
+
return pruned({
|
|
51553
|
+
...compactMessage(m),
|
|
51554
|
+
...cappedRecipients("Cc", addrs(m.CcRecipients)),
|
|
51555
|
+
Sent: m.SentDateTime,
|
|
51556
|
+
Importance: m.Importance,
|
|
51557
|
+
IsDraft: m.IsDraft,
|
|
51558
|
+
ConversationId: m.ConversationId,
|
|
51559
|
+
ParentFolderId: m.ParentFolderId,
|
|
51560
|
+
Categories: m.Categories?.length ? m.Categories : void 0,
|
|
51561
|
+
WebLink: m.WebLink
|
|
51562
|
+
});
|
|
51563
|
+
}
|
|
51564
|
+
function compactEvent(e) {
|
|
51565
|
+
return pruned({
|
|
51566
|
+
Id: e.Id,
|
|
51567
|
+
Subject: e.Subject,
|
|
51568
|
+
Start: e.Start?.DateTime,
|
|
51569
|
+
End: e.End?.DateTime,
|
|
51570
|
+
TimeZone: e.Start?.TimeZone,
|
|
51571
|
+
Location: e.Location?.DisplayName || void 0,
|
|
51572
|
+
Organizer: addr(e.Organizer),
|
|
51573
|
+
IsAllDay: e.IsAllDay,
|
|
51574
|
+
IsCancelled: e.IsCancelled,
|
|
51575
|
+
IsOrganizer: e.IsOrganizer,
|
|
51576
|
+
MyResponse: e.ResponseStatus?.Response,
|
|
51577
|
+
ResponseRequested: e.ResponseRequested,
|
|
51578
|
+
Type: e.Type,
|
|
51579
|
+
ShowAs: e.ShowAs
|
|
51580
|
+
});
|
|
51581
|
+
}
|
|
51582
|
+
function fullEvent(e) {
|
|
51583
|
+
return pruned({
|
|
51584
|
+
...compactEvent(e),
|
|
51585
|
+
SeriesMasterId: e.SeriesMasterId,
|
|
51586
|
+
Attendees: e.Attendees?.map(
|
|
51587
|
+
(a) => pruned({ Who: addr(a), Response: a.Status?.Response })
|
|
51588
|
+
),
|
|
51589
|
+
// The Teams link lives under OnlineMeeting; OnlineMeetingUrl is empty on
|
|
51590
|
+
// every live Teams meeting checked.
|
|
51591
|
+
JoinUrl: e.OnlineMeeting?.JoinUrl || e.OnlineMeetingUrl || void 0,
|
|
51592
|
+
// Kept beside JoinUrl so a consumer of the old field still finds it.
|
|
51593
|
+
OnlineMeetingUrl: e.OnlineMeetingUrl || void 0,
|
|
51594
|
+
Preview: e.BodyPreview?.trim() || void 0,
|
|
51595
|
+
WebLink: e.WebLink
|
|
51596
|
+
});
|
|
51597
|
+
}
|
|
51598
|
+
function compactFolder(f) {
|
|
51599
|
+
return pruned({
|
|
51600
|
+
Id: f.Id,
|
|
51601
|
+
Name: f.DisplayName,
|
|
51602
|
+
Unread: f.UnreadItemCount,
|
|
51603
|
+
Total: f.TotalItemCount,
|
|
51604
|
+
// Listings are one level deep; a non-zero count says there is more below.
|
|
51605
|
+
Children: f.ChildFolderCount
|
|
51606
|
+
});
|
|
51607
|
+
}
|
|
51608
|
+
function stripOData(o) {
|
|
51609
|
+
const out = {};
|
|
51610
|
+
for (const [k, v] of Object.entries(o)) {
|
|
51611
|
+
if (!k.startsWith("@odata.")) out[k] = v;
|
|
51612
|
+
}
|
|
51613
|
+
return out;
|
|
51614
|
+
}
|
|
51615
|
+
function pruned(o) {
|
|
51616
|
+
const out = {};
|
|
51617
|
+
for (const [k, v] of Object.entries(o)) {
|
|
51618
|
+
if (v !== void 0) out[k] = v;
|
|
51619
|
+
}
|
|
51620
|
+
return out;
|
|
51621
|
+
}
|
|
51622
|
+
function projectCollection(payload, project, label) {
|
|
51623
|
+
const items = Array.isArray(payload?.value) ? payload.value : [];
|
|
51624
|
+
return pruned({
|
|
51625
|
+
count: items.length,
|
|
51626
|
+
items: items.map(
|
|
51627
|
+
(item) => projectOrRaw(item, project, { label, context: "office-outlook-mcp" })
|
|
51628
|
+
),
|
|
51629
|
+
nextLink: payload?.["@odata.nextLink"]
|
|
51630
|
+
});
|
|
51631
|
+
}
|
|
51632
|
+
|
|
51633
|
+
// src/tools/_invite.ts
|
|
51634
|
+
var EVENT_EXPAND = "Microsoft.OutlookServices.EventMessage/Event";
|
|
51635
|
+
function resolvedEvent(e) {
|
|
51636
|
+
return e?.Id ? e : void 0;
|
|
51637
|
+
}
|
|
51638
|
+
var INVITE_GONE = "the meeting is no longer on your calendar (already declined, cancelled, or deleted), so there is nothing to respond to";
|
|
51639
|
+
var MEETING_MESSAGE_TYPE = "Microsoft.OutlookServices.EventMessage/MeetingMessageType";
|
|
51640
|
+
function isMeetingMessage(m) {
|
|
51641
|
+
return !!m?.MeetingMessageType || (m?.["@odata.type"] ?? "").includes("EventMessage");
|
|
51642
|
+
}
|
|
51643
|
+
|
|
51644
|
+
// src/tools/_paging.ts
|
|
51645
|
+
init_zod();
|
|
51646
|
+
init_dist2();
|
|
51647
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
51648
|
+
var nextLinkParam = external_exports.string().min(1).optional().describe(
|
|
51649
|
+
"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."
|
|
51650
|
+
);
|
|
51651
|
+
function fetchPage(client2, nextLink, path, opts = {}) {
|
|
51652
|
+
if (nextLink === void 0) return client2.get(path, opts);
|
|
51653
|
+
const { query: _ignored, ...headerOpts } = opts;
|
|
51654
|
+
return client2.getAbsolute(nextLink, headerOpts);
|
|
51655
|
+
}
|
|
51656
|
+
function plainCollection(data) {
|
|
51657
|
+
const items = Array.isArray(data?.value) ? data.value : [];
|
|
51658
|
+
const out = { count: items.length, items: items.map(stripOData) };
|
|
51659
|
+
const next = data?.["@odata.nextLink"];
|
|
51660
|
+
if (next) out.nextLink = next;
|
|
51661
|
+
return out;
|
|
51662
|
+
}
|
|
51663
|
+
var MAX_KEY_LENGTH2 = 16;
|
|
51664
|
+
function idKey(id) {
|
|
51665
|
+
return id.length <= MAX_KEY_LENGTH2 ? id : createHash4("sha256").update(id).digest("base64url").slice(0, 12);
|
|
51666
|
+
}
|
|
51667
|
+
var fromParam = external_exports.string().min(1).optional().describe(
|
|
51668
|
+
'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`.'
|
|
51669
|
+
);
|
|
51670
|
+
var MAX_TOP = 1e3;
|
|
51671
|
+
var FROM_HINT = "Pass a previous next_from unchanged, or an ISO 8601 time such as 2026-10-01T00:00:00Z (one with no zone is read as UTC).";
|
|
51672
|
+
var ISO_DATE_TIME = /^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}(:\d{2}(\.\d+)?)?(Z|[+-]\d{2}:?\d{2})?)?$/i;
|
|
51673
|
+
function utcLiteral(ms) {
|
|
51674
|
+
return new Date(ms).toISOString().replace(/\.000Z$/, "Z");
|
|
51675
|
+
}
|
|
51676
|
+
function parseTime(raw) {
|
|
51677
|
+
if (!ISO_DATE_TIME.test(raw)) return void 0;
|
|
51678
|
+
const t = /T[\d:.]+$/.test(raw) ? `${raw}Z` : raw;
|
|
51679
|
+
const ms = Date.parse(t);
|
|
51680
|
+
if (Number.isNaN(ms)) return void 0;
|
|
51681
|
+
if (new Date(ms + offsetMs(t)).toISOString().slice(0, 10) !== t.slice(0, 10)) return void 0;
|
|
51682
|
+
return ms;
|
|
51683
|
+
}
|
|
51684
|
+
function offsetMs(t) {
|
|
51685
|
+
const m = /([+-])(\d{2}):?(\d{2})$/.exec(t);
|
|
51686
|
+
if (!m || /Z$/i.test(t)) return 0;
|
|
51687
|
+
const mins = Number(m[2]) * 60 + Number(m[3]);
|
|
51688
|
+
return (m[1] === "-" ? -1 : 1) * mins * 6e4;
|
|
51689
|
+
}
|
|
51690
|
+
function encodeFrom(pos) {
|
|
51691
|
+
return Buffer.from(JSON.stringify({ t: pos.t, ids: pos.ids }), "utf8").toString("base64url");
|
|
51692
|
+
}
|
|
51693
|
+
function decodeFrom(from) {
|
|
51694
|
+
if (/^\d{4}-/.test(from)) {
|
|
51695
|
+
const ms2 = parseTime(from);
|
|
51696
|
+
if (ms2 === void 0) {
|
|
51697
|
+
throw new McpToolError(`\`from\` is not a valid ISO 8601 UTC date-time: ${from}`, { hint: FROM_HINT });
|
|
51698
|
+
}
|
|
51699
|
+
return { t: utcLiteral(ms2), ids: [] };
|
|
51700
|
+
}
|
|
51701
|
+
let parsed;
|
|
51702
|
+
try {
|
|
51703
|
+
parsed = /^[A-Za-z0-9_-]+$/.test(from) ? JSON.parse(Buffer.from(from, "base64url").toString("utf8")) : void 0;
|
|
51704
|
+
} catch {
|
|
51705
|
+
parsed = void 0;
|
|
51706
|
+
}
|
|
51707
|
+
const p = parsed;
|
|
51708
|
+
const ms = typeof p?.t === "string" ? parseTime(p.t) : void 0;
|
|
51709
|
+
if (ms === void 0 || !Array.isArray(p?.ids) || !p.ids.every((id) => typeof id === "string")) {
|
|
51710
|
+
throw new McpToolError("`from` is neither an ISO 8601 time nor a next_from token.", { hint: FROM_HINT });
|
|
51711
|
+
}
|
|
51712
|
+
return { t: utcLiteral(ms), ids: [...new Set(p.ids.map(idKey))] };
|
|
51713
|
+
}
|
|
51714
|
+
function fromQuery(pos, limit) {
|
|
51715
|
+
return { bound: `ReceivedDateTime ge ${pos.t}`, top: Math.min(limit + pos.ids.length, MAX_TOP) };
|
|
51716
|
+
}
|
|
51717
|
+
function fromPage(rows, pos, limit, apiHasMore) {
|
|
51718
|
+
const seen = new Set(pos.ids.map(idKey));
|
|
51719
|
+
const fresh = rows.filter((r) => r.Id === void 0 || !seen.has(idKey(r.Id)));
|
|
51720
|
+
const kept = fresh.slice(0, limit);
|
|
51721
|
+
const next = nextPosition(kept, pos);
|
|
51722
|
+
return { rows: kept, next_from: encodeFrom(next), has_more: fresh.length > limit || apiHasMore };
|
|
51723
|
+
}
|
|
51724
|
+
function nextPosition(rows, pos) {
|
|
51725
|
+
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));
|
|
51726
|
+
if (dated.length === 0) return pos;
|
|
51727
|
+
const newest = Math.max(...dated.map((r) => r.ms));
|
|
51728
|
+
const tied = dated.filter((r) => r.ms === newest).map((r) => r.id);
|
|
51729
|
+
const carried = newest === Date.parse(pos.t) ? pos.ids.map(idKey).filter((id) => !tied.includes(id)) : [];
|
|
51730
|
+
return { t: utcLiteral(newest), ids: [...carried, ...tied] };
|
|
51731
|
+
}
|
|
51732
|
+
|
|
51733
|
+
// src/tools/mail.ts
|
|
51734
|
+
var WELL_KNOWN = [
|
|
51735
|
+
"inbox",
|
|
51736
|
+
"drafts",
|
|
51737
|
+
"sentitems",
|
|
51738
|
+
"deleteditems",
|
|
51739
|
+
"archive",
|
|
51740
|
+
"junkemail",
|
|
51741
|
+
"outbox",
|
|
51742
|
+
"clutter"
|
|
51743
|
+
];
|
|
51744
|
+
var LIST_SELECT = "Id,Subject,From,ToRecipients,ReceivedDateTime,IsRead,HasAttachments,BodyPreview";
|
|
51745
|
+
function folderSegment(folder) {
|
|
51746
|
+
return encodeURIComponent(folder);
|
|
51747
|
+
}
|
|
51748
|
+
var UNREAD_SELECT = [
|
|
51749
|
+
"Id",
|
|
51750
|
+
"Subject",
|
|
51751
|
+
"From",
|
|
51752
|
+
"ToRecipients",
|
|
51753
|
+
"CcRecipients",
|
|
51754
|
+
"ReceivedDateTime",
|
|
51755
|
+
"Importance",
|
|
51756
|
+
"HasAttachments",
|
|
51757
|
+
"Categories",
|
|
51758
|
+
"Flag",
|
|
51759
|
+
"ConversationId",
|
|
51760
|
+
MEETING_MESSAGE_TYPE
|
|
51761
|
+
].join(",");
|
|
51762
|
+
var BODY_CONCURRENCY = 4;
|
|
51763
|
+
function triageKind(m) {
|
|
51764
|
+
switch (m.MeetingMessageType) {
|
|
51765
|
+
case "MeetingRequest":
|
|
51766
|
+
return "meetingRequest";
|
|
51767
|
+
case "MeetingCancelled":
|
|
51768
|
+
return "meetingCancelled";
|
|
51769
|
+
case "MeetingAccepted":
|
|
51770
|
+
case "MeetingTenativelyAccepted":
|
|
51771
|
+
// sic — Outlook's spelling
|
|
51772
|
+
case "MeetingTentativelyAccepted":
|
|
51773
|
+
case "MeetingDeclined":
|
|
51774
|
+
return "meetingResponse";
|
|
51775
|
+
}
|
|
51776
|
+
const type = m["@odata.type"] ?? "";
|
|
51777
|
+
if (!type.includes("EventMessage")) return "mail";
|
|
51778
|
+
if (/Request$/.test(type)) return "meetingRequest";
|
|
51779
|
+
if (/Cancel/.test(type)) return "meetingCancelled";
|
|
51780
|
+
return "meetingResponse";
|
|
51781
|
+
}
|
|
51782
|
+
function briefUnread(m) {
|
|
51783
|
+
const who = m.From ?? m.Sender;
|
|
51784
|
+
const sender = who?.EmailAddress?.Name || who?.EmailAddress?.Address;
|
|
51785
|
+
const kind = triageKind(m);
|
|
51786
|
+
const title = sender ? `${sender}: ${m.Subject ?? "(no subject)"}` : m.Subject;
|
|
51787
|
+
return briefLine(title, briefDate(m.ReceivedDateTime), kind === "mail" ? "unread" : spaced(kind));
|
|
51788
|
+
}
|
|
51789
|
+
function triageEvent(e) {
|
|
51790
|
+
return stripUndefined({
|
|
51791
|
+
id: e.Id,
|
|
51792
|
+
subject: e.Subject,
|
|
51793
|
+
start: e.Start?.DateTime,
|
|
51794
|
+
end: e.End?.DateTime,
|
|
51795
|
+
timeZone: e.Start?.TimeZone,
|
|
51796
|
+
location: e.Location?.DisplayName || void 0,
|
|
51797
|
+
organizer: addr(e.Organizer),
|
|
51798
|
+
responseStatus: e.ResponseStatus?.Response,
|
|
51799
|
+
isCancelled: e.IsCancelled
|
|
51800
|
+
});
|
|
51801
|
+
}
|
|
51802
|
+
var INVITE_UNRESOLVED_HINT = `This invite's ${INVITE_GONE}.`;
|
|
51803
|
+
function truncateChars(text, max) {
|
|
51804
|
+
const end = /[\uD800-\uDBFF]/.test(text.charAt(max - 1)) ? max - 1 : max;
|
|
51805
|
+
return text.slice(0, end);
|
|
51806
|
+
}
|
|
51807
|
+
function stripUndefined(o) {
|
|
51808
|
+
return Object.fromEntries(Object.entries(o).filter(([, v]) => v !== void 0));
|
|
51809
|
+
}
|
|
51810
|
+
var ATTACHMENT_META_SELECT = "Id,Name,ContentType,Size,IsInline";
|
|
51811
|
+
var MAX_INLINE_BYTES = 5 * 1024 * 1024;
|
|
51812
|
+
var MAX_DOWNLOAD_BYTES = 50 * 1024 * 1024;
|
|
51813
|
+
var SERVER_NAME = "office-outlook-mcp";
|
|
51814
|
+
var OUTPUT_DIR_ENV = "OUTLOOK_OUTPUT_DIR";
|
|
51815
|
+
function deliveryRoutes(env) {
|
|
51816
|
+
const store = blobStoreFromEnv(env);
|
|
51817
|
+
if (isHostedEnv(env)) return { hosted: true, ...store ? { store } : {} };
|
|
51818
|
+
const configured = readEnvVar(OUTPUT_DIR_ENV, { env });
|
|
51819
|
+
if (configured !== void 0) return { hosted: false, diskDir: expandPath(configured) };
|
|
51820
|
+
const downloads = join11(readEnvVar("HOME", { env }) ?? homedir6(), "Downloads");
|
|
51821
|
+
return { hosted: false, ...existsSync6(downloads) ? { diskDir: join11(downloads, SERVER_NAME) } : {} };
|
|
51822
|
+
}
|
|
51823
|
+
function noRouteHint(routes, why) {
|
|
51824
|
+
return routes.hosted ? `${why} This hosted server has no blob store configured (MCP_BLOB_BASE_URL / MCP_BLOB_SIGNING_KEY), so it cannot hand over a download link. Open it in Outlook to read it.` : `${why} Set ${OUTPUT_DIR_ENV} (or create ~/Downloads) to have it saved to disk, or open it in Outlook.`;
|
|
51825
|
+
}
|
|
51826
|
+
var MAX_TEXT_BYTES = 200 * 1024;
|
|
51827
|
+
var TEXT_TYPE = /^text\/|^application\/(json|xml|csv|ics|calendar)\b|\+(json|xml)$/i;
|
|
51828
|
+
var TEXT_EXT = /\.(txt|csv|tsv|json|xml|ics|md|log|html?)$/i;
|
|
51829
|
+
function attachmentKind(a) {
|
|
51830
|
+
const type = a["@odata.type"] ?? "";
|
|
51831
|
+
if (/ItemAttachment$/.test(type)) return "item";
|
|
51832
|
+
if (/ReferenceAttachment$/.test(type)) return "reference";
|
|
51833
|
+
return "file";
|
|
51834
|
+
}
|
|
51835
|
+
function isImage(contentType) {
|
|
51836
|
+
return /^image\//i.test(contentType) && !/svg/i.test(contentType);
|
|
51837
|
+
}
|
|
51838
|
+
function isText(contentType, name) {
|
|
51839
|
+
return TEXT_TYPE.test(contentType) || /svg/i.test(contentType) || TEXT_EXT.test(name);
|
|
51840
|
+
}
|
|
51841
|
+
function base64DecodedLength(b64) {
|
|
51842
|
+
const padding = b64.endsWith("==") ? 2 : b64.endsWith("=") ? 1 : 0;
|
|
51843
|
+
return Math.floor(b64.length * 3 / 4) - padding;
|
|
51844
|
+
}
|
|
51845
|
+
function utf8Boundary(buf, max) {
|
|
51846
|
+
if (buf.length <= max) return buf.length;
|
|
51847
|
+
let end = max;
|
|
51848
|
+
while (end > 0 && (buf[end] & 192) === 128) end--;
|
|
51849
|
+
return end;
|
|
51850
|
+
}
|
|
51851
|
+
function sinceLiteral(hours) {
|
|
51852
|
+
return new Date(Date.now() - hours * 36e5).toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
51853
|
+
}
|
|
51854
|
+
function fromPosition(from, conflicts) {
|
|
51855
|
+
if (from === void 0) return void 0;
|
|
51856
|
+
for (const [name, value] of Object.entries(conflicts)) {
|
|
51857
|
+
if (value === void 0) continue;
|
|
51858
|
+
throw new McpToolError(`\`from\` and \`${name}\` cannot be combined.`, {
|
|
51859
|
+
hint: name === "nextLink" ? "To continue a `from` listing, pass its `next_from` as `from`; `nextLink` belongs to the newest-first listing." : `\`from\` pages oldest first by time; drop \`${name}\`, or omit \`from\` for the newest-first listing.`
|
|
51860
|
+
});
|
|
51861
|
+
}
|
|
51862
|
+
return decodeFrom(from);
|
|
51863
|
+
}
|
|
51864
|
+
var FROM_DESCRIPTION = "Pass `from` (an ISO 8601 UTC time, or a previous `next_from`) to page OLDEST first from that time instead: the result carries `next_from` (pass it back unchanged as `from` for the next page; on an empty page it is the same position, so it can be polled for new arrivals) and `has_more` (more already waiting), and no nextLink. Backfill: pass an old `from` and keep passing `next_from` until `has_more` is false; then keep polling with the last `next_from` for new mail.";
|
|
51865
|
+
async function listFrom(client2, pos, opts) {
|
|
51866
|
+
const { bound, top } = fromQuery(pos, opts.limit);
|
|
51867
|
+
const data = await client2.get(
|
|
51868
|
+
`/me/mailfolders/${folderSegment(opts.folder ?? "inbox")}/messages`,
|
|
51869
|
+
{
|
|
51870
|
+
query: {
|
|
51871
|
+
$top: top,
|
|
51872
|
+
$select: LIST_SELECT,
|
|
51873
|
+
// The date bound leads: Outlook rejects a $filter that does not start with the $orderby property.
|
|
51874
|
+
$filter: opts.unreadOnly === true ? `${bound} and IsRead eq false` : bound,
|
|
51875
|
+
$orderby: "ReceivedDateTime asc"
|
|
51876
|
+
}
|
|
51877
|
+
}
|
|
51878
|
+
);
|
|
51879
|
+
const listed = Array.isArray(data?.value) ? data.value : [];
|
|
51880
|
+
const page = fromPage(listed, pos, opts.limit, Boolean(data?.["@odata.nextLink"]));
|
|
51881
|
+
const { "@odata.nextLink": _superseded, ...rest } = data ?? {};
|
|
51882
|
+
const body = opts.view === "raw" ? { ...rest, value: page.rows } : projectCollection({ value: page.rows }, opts.view === "full" ? fullMessage : compactMessage, "message");
|
|
51883
|
+
return mailboxUntrusted({ ...body, next_from: page.next_from, has_more: page.has_more });
|
|
51884
|
+
}
|
|
51885
|
+
function registerMailTools(server, client2, opts = {}) {
|
|
51886
|
+
const env = opts.env ?? process.env;
|
|
51887
|
+
server.registerTool(
|
|
51888
|
+
"outlook_list_folders",
|
|
51889
|
+
{
|
|
51890
|
+
description: "List mail folders with unread and total counts. Use this to discover folder ids before listing messages; the well-known names (" + WELL_KNOWN.join(", ") + ") can be used directly without a lookup. Lists ONE level: top-level folders by default, or the child folders of `parent`. A folder with `Children` > 0 has subfolders \u2014 list them with `parent` set to its Id.",
|
|
51891
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
51892
|
+
_meta: workMeta("lookup"),
|
|
51893
|
+
inputSchema: external_exports.object({
|
|
51894
|
+
view: viewParam(VIEWS2),
|
|
50834
51895
|
parent: external_exports.string().min(1).optional().describe("List the child folders of this folder (id or well-known name). Omit for the top level."),
|
|
50835
51896
|
limit: external_exports.number().int().min(1).max(200).optional().describe("Max folders (default 50)"),
|
|
50836
51897
|
nextLink: nextLinkParam
|
|
@@ -51050,7 +52111,7 @@ function registerMailTools(server, client2) {
|
|
|
51050
52111
|
server.registerTool(
|
|
51051
52112
|
"outlook_get_attachment",
|
|
51052
52113
|
{
|
|
51053
|
-
description: "Get one attachment's content (ids from outlook_list_attachments).
|
|
52114
|
+
description: "Get one attachment's content (ids from outlook_list_attachments). A text-like file (text/*, JSON, CSV, XML, .ics) up to 5 MB is decoded to `text`, capped at 200 KB (`textTruncated` marks a cut). An image up to 5 MB comes back as image content the model can see. Anything else (PDF, Office, archives) or larger, up to 50 MB, is handed over as the file itself: on a hosted server a signed, expiring download link (`downloadUrl`, valid until `downloadExpiresAt`) \u2014 give it to the user; on a local install a fresh file (`path`) under OUTLOOK_OUTPUT_DIR, else ~/Downloads/office-outlook-mcp. `via` says which (image, resource, link or disk). Where neither route exists the result is metadata only and `hint` says why. An attached Outlook item (a forwarded email or event) or a cloud-file link has no bytes to return; the result says which. " + UNTRUSTED_DESCRIPTION_SUFFIX,
|
|
51054
52115
|
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
51055
52116
|
_meta: workMeta("lookup"),
|
|
51056
52117
|
inputSchema: external_exports.object({
|
|
@@ -51083,12 +52144,22 @@ function registerMailTools(server, client2) {
|
|
|
51083
52144
|
return mailboxUntrusted(out);
|
|
51084
52145
|
}
|
|
51085
52146
|
const image = isImage(contentType);
|
|
51086
|
-
|
|
51087
|
-
|
|
51088
|
-
|
|
51089
|
-
|
|
51090
|
-
if (
|
|
51091
|
-
|
|
52147
|
+
const text = !image && isText(contentType, name);
|
|
52148
|
+
const size = meta3.Size ?? 0;
|
|
52149
|
+
const routes = deliveryRoutes(env);
|
|
52150
|
+
const canHandOver = routes.store !== void 0 || routes.diskDir !== void 0;
|
|
52151
|
+
if (!canHandOver) {
|
|
52152
|
+
if (!image && !text) {
|
|
52153
|
+
out.hint = noRouteHint(routes, "Binary content is not inlined: only images and text-like files are returned.");
|
|
52154
|
+
return mailboxUntrusted(out);
|
|
52155
|
+
}
|
|
52156
|
+
if (size > MAX_INLINE_BYTES) {
|
|
52157
|
+
out.hint = noRouteHint(routes, "Content is not inlined above 5 MB.");
|
|
52158
|
+
return mailboxUntrusted(out);
|
|
52159
|
+
}
|
|
52160
|
+
}
|
|
52161
|
+
if (size > MAX_DOWNLOAD_BYTES) {
|
|
52162
|
+
out.hint = "Attachments over 50 MB are not downloaded. Open it in Outlook to read it.";
|
|
51092
52163
|
return mailboxUntrusted(out);
|
|
51093
52164
|
}
|
|
51094
52165
|
const full = await client2.get(path);
|
|
@@ -51097,19 +52168,44 @@ function registerMailTools(server, client2) {
|
|
|
51097
52168
|
out.hint = "Outlook returned no content for this attachment.";
|
|
51098
52169
|
return mailboxUntrusted(out);
|
|
51099
52170
|
}
|
|
51100
|
-
|
|
51101
|
-
|
|
52171
|
+
const decodedSize = base64DecodedLength(bytes);
|
|
52172
|
+
if (decodedSize > MAX_DOWNLOAD_BYTES) {
|
|
52173
|
+
out.hint = "Attachments over 50 MB are not downloaded. Open it in Outlook to read it.";
|
|
52174
|
+
return mailboxUntrusted(out);
|
|
52175
|
+
}
|
|
52176
|
+
if (decodedSize > MAX_INLINE_BYTES && !canHandOver) {
|
|
52177
|
+
out.hint = noRouteHint(routes, "Content is not inlined above 5 MB.");
|
|
51102
52178
|
return mailboxUntrusted(out);
|
|
51103
52179
|
}
|
|
51104
52180
|
const buf = Buffer.from(bytes, "base64");
|
|
51105
|
-
|
|
51106
|
-
|
|
51107
|
-
|
|
51108
|
-
|
|
52181
|
+
if (text && buf.length <= MAX_INLINE_BYTES) {
|
|
52182
|
+
out.inlined = true;
|
|
52183
|
+
out.text = buf.subarray(0, utf8Boundary(buf, MAX_TEXT_BYTES)).toString("utf8");
|
|
52184
|
+
if (buf.length > MAX_TEXT_BYTES) out.textTruncated = true;
|
|
52185
|
+
return mailboxUntrusted(out);
|
|
51109
52186
|
}
|
|
51110
|
-
|
|
51111
|
-
|
|
51112
|
-
|
|
52187
|
+
const delivered = await deliverFile({
|
|
52188
|
+
// A standalone copy: a small Buffer shares Node's allocation pool, and
|
|
52189
|
+
// mcp-utils' blob PUT (3.7.1) sends `view.slice().buffer` — for a Buffer
|
|
52190
|
+
// that is the WHOLE pool, i.e. unrelated process memory.
|
|
52191
|
+
bytes: new Uint8Array(buf),
|
|
52192
|
+
fileName: name || "attachment",
|
|
52193
|
+
...contentType ? { mimeType: contentType } : {},
|
|
52194
|
+
store: routes.store ?? null,
|
|
52195
|
+
keyPrefix: "attachments",
|
|
52196
|
+
inlineMaxBytes: MAX_INLINE_BYTES,
|
|
52197
|
+
// Locally a non-image is saved rather than inlined as an embedded resource.
|
|
52198
|
+
prefer: image || routes.store !== void 0 ? "auto" : "disk",
|
|
52199
|
+
...routes.diskDir !== void 0 ? { disk: { dir: routes.diskDir, mode: 384 } } : {}
|
|
52200
|
+
});
|
|
52201
|
+
out.inlined = delivered.via === "image" || delivered.via === "resource";
|
|
52202
|
+
out.via = delivered.via;
|
|
52203
|
+
if (delivered.url !== void 0) out.downloadUrl = delivered.url;
|
|
52204
|
+
if (delivered.expiresAt !== void 0) out.downloadExpiresAt = delivered.expiresAt;
|
|
52205
|
+
if (delivered.path !== void 0) out.path = delivered.path;
|
|
52206
|
+
if (delivered.note !== void 0) out.deliveryNote = delivered.note;
|
|
52207
|
+
const result = mailboxUntrusted(out);
|
|
52208
|
+
return { ...result, content: [...result.content, ...delivered.content] };
|
|
51113
52209
|
}
|
|
51114
52210
|
);
|
|
51115
52211
|
}
|
|
@@ -51610,1394 +52706,781 @@ ${existing}` : comment };
|
|
|
51610
52706
|
var FLAG_STATUS = { flagged: "Flagged", complete: "Complete", none: "NotFlagged" };
|
|
51611
52707
|
var UPDATE_CONCURRENCY = 4;
|
|
51612
52708
|
var sameCategories = (a, b) => [...a ?? []].map((c) => c.toLowerCase()).sort().join("\n") === [...b ?? []].map((c) => c.toLowerCase()).sort().join("\n");
|
|
51613
|
-
function editCategories(current, add, remove) {
|
|
51614
|
-
const gone = new Set(remove.map((c) => c.toLowerCase()));
|
|
51615
|
-
const next = (current ?? []).filter((c) => !gone.has(c.toLowerCase()));
|
|
51616
|
-
for (const c of add) {
|
|
51617
|
-
if (!next.some((n) => n.toLowerCase() === c.toLowerCase())) next.push(c);
|
|
51618
|
-
}
|
|
51619
|
-
return next;
|
|
51620
|
-
}
|
|
51621
|
-
function unchangedFields(payload, after) {
|
|
51622
|
-
const out = [];
|
|
51623
|
-
if (payload.IsRead !== void 0 && after.IsRead !== payload.IsRead) out.push("IsRead");
|
|
51624
|
-
if (payload.Flag && after.Flag?.FlagStatus !== payload.Flag.FlagStatus) out.push("Flag");
|
|
51625
|
-
if (payload.Categories && !sameCategories(after.Categories, payload.Categories)) out.push("Categories");
|
|
51626
|
-
return out;
|
|
51627
|
-
}
|
|
51628
|
-
var createEventInput = external_exports.object({
|
|
51629
|
-
subject: external_exports.string().describe("Event title"),
|
|
51630
|
-
start: external_exports.string().min(1).describe("Start, ISO 8601 local time e.g. 2026-09-22T15:00:00"),
|
|
51631
|
-
end: external_exports.string().min(1).describe("End, ISO 8601 local time"),
|
|
51632
|
-
timeZone: external_exports.string().optional().describe("Windows time-zone name. Defaults to the MAILBOX time zone."),
|
|
51633
|
-
location: external_exports.string().optional().describe("Location display name"),
|
|
51634
|
-
body: external_exports.string().optional().describe("Event description"),
|
|
51635
|
-
attendees: recipientList.describe("Required attendees' email addresses"),
|
|
51636
|
-
optionalAttendees: recipientList.describe("Optional attendees' email addresses"),
|
|
51637
|
-
teamsMeeting: teamsMeetingParam,
|
|
51638
|
-
confirmToken: confirmTokenParam
|
|
51639
|
-
});
|
|
51640
|
-
function registerWriteTools(server, client2) {
|
|
51641
|
-
server.registerTool(
|
|
51642
|
-
"outlook_send_message",
|
|
51643
|
-
{
|
|
51644
|
-
description: "Send an email from the signed-in mailbox. " + CONFIRM_FLOW_SENTENCE + " The preview shows exactly what would be sent. " + TIMEOUT_SENTENCE + " " + CONFIRM_INJECTION_RULE,
|
|
51645
|
-
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
|
|
51646
|
-
_meta: workMeta("send", { aliases: ["email", "send an email", "mail"] }),
|
|
51647
|
-
inputSchema: external_exports.object({
|
|
51648
|
-
to: recipientList,
|
|
51649
|
-
cc: recipientList,
|
|
51650
|
-
bcc: recipientList,
|
|
51651
|
-
subject: external_exports.string().describe("Subject line"),
|
|
51652
|
-
body: external_exports.string().describe("Message body"),
|
|
51653
|
-
html: external_exports.boolean().optional().describe("Send the body as HTML (default: plain text)"),
|
|
51654
|
-
saveToSentItems: external_exports.boolean().optional().describe("Keep a copy in Sent Items (default true)"),
|
|
51655
|
-
confirmToken: confirmTokenParam
|
|
51656
|
-
})
|
|
51657
|
-
},
|
|
51658
|
-
async ({ to, cc, bcc, subject, body, html, saveToSentItems, confirmToken }, ctx) => {
|
|
51659
|
-
const payload = {
|
|
51660
|
-
Message: {
|
|
51661
|
-
...messageBody(subject, body, html),
|
|
51662
|
-
ToRecipients: toRecipients(to),
|
|
51663
|
-
...cc?.length ? { CcRecipients: toRecipients(cc) } : {},
|
|
51664
|
-
...bcc?.length ? { BccRecipients: toRecipients(bcc) } : {}
|
|
51665
|
-
},
|
|
51666
|
-
SaveToSentItems: saveToSentItems !== false
|
|
51667
|
-
};
|
|
51668
|
-
const recipients = [...to ?? [], ...cc ?? [], ...bcc ?? []].join(", ");
|
|
51669
|
-
const gate = await confirmWrite(ctx, {
|
|
51670
|
-
tool: "outlook_send_message",
|
|
51671
|
-
action: "mail.send",
|
|
51672
|
-
message: "Review and confirm this email before it is sent:",
|
|
51673
|
-
summary: `Send mail "${subject}" to ${recipients || "(no recipients)"}`,
|
|
51674
|
-
// One signed-in mailbox per server process.
|
|
51675
|
-
account: void 0,
|
|
51676
|
-
request: { method: "POST", path: "/me/sendmail", body: payload },
|
|
51677
|
-
confirmToken
|
|
51678
|
-
});
|
|
51679
|
-
if (gate) return gate;
|
|
51680
|
-
const sent = await writeOrUnknown2(() => client2.write("POST", "/me/sendmail", payload), "Sent Items");
|
|
51681
|
-
if (!sent.ok) return sent.result;
|
|
51682
|
-
return minifiedResult({ sent: true, subject, recipients: recipients || null });
|
|
51683
|
-
}
|
|
51684
|
-
);
|
|
51685
|
-
server.registerTool(
|
|
51686
|
-
"outlook_create_draft",
|
|
51687
|
-
{
|
|
51688
|
-
description: "Create a draft message in the Drafts folder without sending it. Returns the created draft, which can be reviewed and sent from Outlook. It does not ask to confirm: a draft reaches nobody until it is sent from Outlook. Calling it twice leaves two drafts. " + TIMEOUT_SENTENCE,
|
|
51689
|
-
// Self-only and additive, like outlook_reply with draftOnly: nothing is
|
|
51690
|
-
// overwritten or sent. Not idempotent: a repeat creates a second draft.
|
|
51691
|
-
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
51692
|
-
_meta: workMeta("change", { aliases: ["draft", "draft an email"] }),
|
|
51693
|
-
inputSchema: external_exports.object({
|
|
51694
|
-
to: recipientList,
|
|
51695
|
-
cc: recipientList,
|
|
51696
|
-
subject: external_exports.string().describe("Subject line"),
|
|
51697
|
-
body: external_exports.string().describe("Message body"),
|
|
51698
|
-
html: external_exports.boolean().optional().describe("Compose the body as HTML (default: plain text)")
|
|
51699
|
-
})
|
|
51700
|
-
},
|
|
51701
|
-
async ({ to, cc, subject, body, html }) => {
|
|
51702
|
-
const payload = {
|
|
51703
|
-
...messageBody(subject, body, html),
|
|
51704
|
-
ToRecipients: toRecipients(to),
|
|
51705
|
-
...cc?.length ? { CcRecipients: toRecipients(cc) } : {}
|
|
51706
|
-
};
|
|
51707
|
-
const create = await writeOrUnknown2(
|
|
51708
|
-
() => client2.write("POST", "/me/messages", payload),
|
|
51709
|
-
"Drafts"
|
|
51710
|
-
);
|
|
51711
|
-
if (!create.ok) return create.result;
|
|
51712
|
-
const created = create.value;
|
|
51713
|
-
return minifiedResult({ created: true, Id: created?.Id, WebLink: created?.WebLink });
|
|
51714
|
-
}
|
|
51715
|
-
);
|
|
51716
|
-
server.registerTool(
|
|
51717
|
-
"outlook_mark_read",
|
|
51718
|
-
{
|
|
51719
|
-
description: "Mark a message read or unread. The result is verified by re-reading the message \u2014 a 2xx alone is not proof it persisted. " + CONFIRM_FLOW_SENTENCE,
|
|
51720
|
-
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
51721
|
-
_meta: workMeta("change"),
|
|
51722
|
-
inputSchema: external_exports.object({
|
|
51723
|
-
id: external_exports.string().min(1).describe("Message Id"),
|
|
51724
|
-
isRead: external_exports.boolean().describe("true to mark read, false to mark unread"),
|
|
51725
|
-
confirmToken: confirmTokenParam
|
|
51726
|
-
})
|
|
51727
|
-
},
|
|
51728
|
-
async ({ id, isRead, confirmToken }, ctx) => {
|
|
51729
|
-
const path = `/me/messages/${encodeURIComponent(id)}`;
|
|
51730
|
-
const payload = { IsRead: isRead };
|
|
51731
|
-
const gate = await confirmWrite(ctx, {
|
|
51732
|
-
tool: "outlook_mark_read",
|
|
51733
|
-
action: "mail.mark_read",
|
|
51734
|
-
message: "Review and confirm this change:",
|
|
51735
|
-
summary: `Mark message ${id} as ${isRead ? "read" : "unread"}`,
|
|
51736
|
-
// One signed-in mailbox per server process.
|
|
51737
|
-
account: void 0,
|
|
51738
|
-
target: id,
|
|
51739
|
-
request: { method: "PATCH", path, body: payload },
|
|
51740
|
-
confirmToken
|
|
51741
|
-
});
|
|
51742
|
-
if (gate) return gate;
|
|
51743
|
-
await client2.write("PATCH", path, payload);
|
|
51744
|
-
const after = await client2.get(`${path}?$select=IsRead`);
|
|
51745
|
-
return minifiedResult({
|
|
51746
|
-
updated: after?.IsRead === isRead,
|
|
51747
|
-
IsRead: after?.IsRead,
|
|
51748
|
-
...after?.IsRead === isRead ? {} : { warning: "Outlook accepted the write but the value did not change." }
|
|
51749
|
-
});
|
|
51750
|
-
}
|
|
51751
|
-
);
|
|
51752
|
-
server.registerTool(
|
|
51753
|
-
"outlook_update_message",
|
|
51754
|
-
{
|
|
51755
|
-
description: 'Flag, categorise and/or mark read up to 50 messages at once \u2014 the filing step of the triage loop (outlook_list_unread_messages \u2192 act \u2192 update). `flag`: "flagged", "complete" or "none". `categories` replaces a message\'s categories; `addCategories`/`removeCategories` edit each message\'s current list instead (names from outlook_list_categories). Duplicate ids are collapsed, so each message is written once. Each id gets its own result `{ id, ok, error? }`, verified against what Outlook stored; one failure does not stop the rest, but a rejected or missing credential fails the whole call. Only your mailbox changes \u2014 nobody is notified. ' + CONFIRM_FLOW_SENTENCE,
|
|
51756
|
-
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
51757
|
-
_meta: workMeta("change", { aliases: ["flag", "categorize"] }),
|
|
51758
|
-
inputSchema: external_exports.object({
|
|
51759
|
-
messageIds: external_exports.array(external_exports.string().min(1)).min(1).max(50).describe("Message Ids (1 to 50)"),
|
|
51760
|
-
flag: external_exports.enum(["flagged", "complete", "none"]).optional().describe("Follow-up flag to set"),
|
|
51761
|
-
categories: external_exports.array(external_exports.string().min(1)).optional().describe("Replace the categories with exactly these ([] clears them)"),
|
|
51762
|
-
addCategories: external_exports.array(external_exports.string().min(1)).optional().describe("Categories to add"),
|
|
51763
|
-
removeCategories: external_exports.array(external_exports.string().min(1)).optional().describe("Categories to remove"),
|
|
51764
|
-
isRead: external_exports.boolean().optional().describe("true to mark read, false to mark unread"),
|
|
51765
|
-
confirmToken: confirmTokenParam
|
|
51766
|
-
})
|
|
51767
|
-
},
|
|
51768
|
-
async ({ messageIds, flag, categories, addCategories, removeCategories, isRead, confirmToken }, ctx) => {
|
|
51769
|
-
const add = addCategories ?? [];
|
|
51770
|
-
const remove = removeCategories ?? [];
|
|
51771
|
-
const editing = add.length > 0 || remove.length > 0;
|
|
51772
|
-
if (categories !== void 0 && editing) {
|
|
51773
|
-
throw new McpToolError("Pass `categories` or `addCategories`/`removeCategories`, not both.", {
|
|
51774
|
-
hint: "`categories` replaces the whole list; the add/remove pair edits the current one."
|
|
51775
|
-
});
|
|
51776
|
-
}
|
|
51777
|
-
const fixed = {
|
|
51778
|
-
...isRead !== void 0 ? { IsRead: isRead } : {},
|
|
51779
|
-
...flag ? { Flag: { FlagStatus: FLAG_STATUS[flag] } } : {},
|
|
51780
|
-
...categories !== void 0 ? { Categories: categories } : {}
|
|
51781
|
-
};
|
|
51782
|
-
if (Object.keys(fixed).length === 0 && !editing) {
|
|
51783
|
-
throw new McpToolError("Nothing to update.", {
|
|
51784
|
-
hint: "Pass at least one of flag, isRead, categories, addCategories or removeCategories."
|
|
51785
|
-
});
|
|
51786
|
-
}
|
|
51787
|
-
const ids = [...new Set(messageIds)];
|
|
51788
|
-
const pathOf = (id) => `/me/messages/${encodeURIComponent(id)}`;
|
|
51789
|
-
const changes = [
|
|
51790
|
-
...flag ? [`flag ${flag}`] : [],
|
|
51791
|
-
...isRead !== void 0 ? [isRead ? "mark read" : "mark unread"] : [],
|
|
51792
|
-
...categories !== void 0 ? [`set categories [${categories.join(", ")}]`] : [],
|
|
51793
|
-
...add.length ? [`add categories [${add.join(", ")}]`] : [],
|
|
51794
|
-
...remove.length ? [`remove categories [${remove.join(", ")}]`] : []
|
|
51795
|
-
];
|
|
51796
|
-
const gate = await confirmWrite(ctx, {
|
|
51797
|
-
tool: "outlook_update_message",
|
|
51798
|
-
action: "mail.update",
|
|
51799
|
-
message: "Review and confirm this change:",
|
|
51800
|
-
summary: `Update ${ids.length} message${ids.length === 1 ? "" : "s"}: ${changes.join("; ")}`,
|
|
51801
|
-
// One signed-in mailbox per server process.
|
|
51802
|
-
account: void 0,
|
|
51803
|
-
target: ids.join(","),
|
|
51804
|
-
request: {
|
|
51805
|
-
method: "PATCH",
|
|
51806
|
-
path: ids.length === 1 ? pathOf(ids[0]) : "/me/messages/{id}",
|
|
51807
|
-
body: editing ? { ...fixed, Categories: `(each message's current categories${add.length ? ` + ${add.join(", ")}` : ""}${remove.length ? ` - ${remove.join(", ")}` : ""})` } : fixed
|
|
51808
|
-
},
|
|
51809
|
-
confirmToken
|
|
51810
|
-
});
|
|
51811
|
-
if (gate) return gate;
|
|
51812
|
-
const results = await mapWithConcurrency(ids, UPDATE_CONCURRENCY, async (id) => {
|
|
51813
|
-
try {
|
|
51814
|
-
const payload = { ...fixed };
|
|
51815
|
-
if (editing) {
|
|
51816
|
-
const current = await client2.get(`${pathOf(id)}?$select=Categories`);
|
|
51817
|
-
payload.Categories = editCategories(current?.Categories, add, remove);
|
|
51818
|
-
}
|
|
51819
|
-
const echoed = await client2.write("PATCH", pathOf(id), payload);
|
|
51820
|
-
const after = echoed && typeof echoed === "object" ? echoed : await client2.get(`${pathOf(id)}?$select=IsRead,Flag,Categories`) ?? {};
|
|
51821
|
-
const unchanged = unchangedFields(payload, after);
|
|
51822
|
-
return unchanged.length ? { id, ok: false, error: `Outlook accepted the write but these did not change: ${unchanged.join(", ")}.` } : { id, ok: true };
|
|
51823
|
-
} catch (e) {
|
|
51824
|
-
if (isCredentialFailure(e)) throw e;
|
|
51825
|
-
return { id, ok: false, error: e instanceof Error ? e.message : String(e) };
|
|
51826
|
-
}
|
|
51827
|
-
});
|
|
51828
|
-
const updated = results.filter((r) => r.ok).length;
|
|
51829
|
-
return minifiedResult({ updated, failed: results.length - updated, results });
|
|
51830
|
-
}
|
|
51831
|
-
);
|
|
52709
|
+
function editCategories(current, add, remove) {
|
|
52710
|
+
const gone = new Set(remove.map((c) => c.toLowerCase()));
|
|
52711
|
+
const next = (current ?? []).filter((c) => !gone.has(c.toLowerCase()));
|
|
52712
|
+
for (const c of add) {
|
|
52713
|
+
if (!next.some((n) => n.toLowerCase() === c.toLowerCase())) next.push(c);
|
|
52714
|
+
}
|
|
52715
|
+
return next;
|
|
52716
|
+
}
|
|
52717
|
+
function unchangedFields(payload, after) {
|
|
52718
|
+
const out = [];
|
|
52719
|
+
if (payload.IsRead !== void 0 && after.IsRead !== payload.IsRead) out.push("IsRead");
|
|
52720
|
+
if (payload.Flag && after.Flag?.FlagStatus !== payload.Flag.FlagStatus) out.push("Flag");
|
|
52721
|
+
if (payload.Categories && !sameCategories(after.Categories, payload.Categories)) out.push("Categories");
|
|
52722
|
+
return out;
|
|
52723
|
+
}
|
|
52724
|
+
var createEventInput = external_exports.object({
|
|
52725
|
+
subject: external_exports.string().describe("Event title"),
|
|
52726
|
+
start: external_exports.string().min(1).describe("Start, ISO 8601 local time e.g. 2026-09-22T15:00:00"),
|
|
52727
|
+
end: external_exports.string().min(1).describe("End, ISO 8601 local time"),
|
|
52728
|
+
timeZone: external_exports.string().optional().describe("Windows time-zone name. Defaults to the MAILBOX time zone."),
|
|
52729
|
+
location: external_exports.string().optional().describe("Location display name"),
|
|
52730
|
+
body: external_exports.string().optional().describe("Event description"),
|
|
52731
|
+
attendees: recipientList.describe("Required attendees' email addresses"),
|
|
52732
|
+
optionalAttendees: recipientList.describe("Optional attendees' email addresses"),
|
|
52733
|
+
teamsMeeting: teamsMeetingParam,
|
|
52734
|
+
confirmToken: confirmTokenParam
|
|
52735
|
+
});
|
|
52736
|
+
function registerWriteTools(server, client2) {
|
|
51832
52737
|
server.registerTool(
|
|
51833
|
-
"
|
|
52738
|
+
"outlook_send_message",
|
|
51834
52739
|
{
|
|
51835
|
-
description:
|
|
51836
|
-
annotations: { readOnlyHint: false, destructiveHint:
|
|
51837
|
-
_meta: workMeta("
|
|
52740
|
+
description: "Send an email from the signed-in mailbox. " + CONFIRM_FLOW_SENTENCE + " The preview shows exactly what would be sent. " + TIMEOUT_SENTENCE + " " + CONFIRM_INJECTION_RULE,
|
|
52741
|
+
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
|
|
52742
|
+
_meta: workMeta("send", { aliases: ["email", "send an email", "mail"] }),
|
|
51838
52743
|
inputSchema: external_exports.object({
|
|
51839
|
-
|
|
51840
|
-
|
|
52744
|
+
to: recipientList,
|
|
52745
|
+
cc: recipientList,
|
|
52746
|
+
bcc: recipientList,
|
|
52747
|
+
subject: external_exports.string().describe("Subject line"),
|
|
52748
|
+
body: external_exports.string().describe("Message body"),
|
|
52749
|
+
html: external_exports.boolean().optional().describe("Send the body as HTML (default: plain text)"),
|
|
52750
|
+
saveToSentItems: external_exports.boolean().optional().describe("Keep a copy in Sent Items (default true)"),
|
|
51841
52751
|
confirmToken: confirmTokenParam
|
|
51842
52752
|
})
|
|
51843
52753
|
},
|
|
51844
|
-
async ({
|
|
51845
|
-
const path = `/me/messages/${encodeURIComponent(id)}/move`;
|
|
51846
|
-
const payload = { DestinationId: destination };
|
|
51847
|
-
const gate = await confirmWrite(ctx, {
|
|
51848
|
-
tool: "outlook_move_message",
|
|
51849
|
-
action: "mail.move",
|
|
51850
|
-
message: "Review and confirm this move:",
|
|
51851
|
-
summary: `Move message ${id} to ${destination}`,
|
|
51852
|
-
// One signed-in mailbox per server process.
|
|
51853
|
-
account: void 0,
|
|
51854
|
-
target: id,
|
|
51855
|
-
request: { method: "POST", path, body: payload },
|
|
51856
|
-
confirmToken
|
|
51857
|
-
});
|
|
51858
|
-
if (gate) return gate;
|
|
51859
|
-
const moved = await client2.write(
|
|
51860
|
-
"POST",
|
|
51861
|
-
path,
|
|
51862
|
-
payload
|
|
51863
|
-
);
|
|
51864
|
-
return minifiedResult({
|
|
51865
|
-
moved: true,
|
|
51866
|
-
newId: moved?.Id,
|
|
51867
|
-
ParentFolderId: moved?.ParentFolderId,
|
|
51868
|
-
note: "The message has a new Id after a move; the old one no longer resolves."
|
|
51869
|
-
});
|
|
51870
|
-
}
|
|
51871
|
-
);
|
|
51872
|
-
server.registerTool(
|
|
51873
|
-
"outlook_create_event",
|
|
51874
|
-
{
|
|
51875
|
-
description: 'Create a calendar event or meeting. A Microsoft Teams meeting is attached by default (`teamsMeeting: false` to skip) and its join link is returned. `timeZone` takes a WINDOWS zone name such as "Eastern Standard Time", not an IANA name. Attendees are emailed an invitation. To pick a time first, use outlook_find_meeting_times: each suggestion it returns carries `createEventArgs` that can be passed here as-is (add `subject` if the search did not set one). ' + CONFIRM_FLOW_SENTENCE + " " + TIMEOUT_SENTENCE + " " + CONFIRM_INJECTION_RULE,
|
|
51876
|
-
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
|
|
51877
|
-
_meta: workMeta("change", { aliases: ["book a meeting", "schedule a meeting", "invite"] }),
|
|
51878
|
-
inputSchema: createEventInput
|
|
51879
|
-
},
|
|
51880
|
-
async ({ subject, start, end, timeZone, location, body, attendees, optionalAttendees, teamsMeeting, confirmToken }, ctx) => {
|
|
51881
|
-
const wantTeams = teamsMeeting !== false;
|
|
51882
|
-
const allAttendees = [
|
|
51883
|
-
...(attendees ?? []).map((a) => attendee(a, "Required")),
|
|
51884
|
-
...(optionalAttendees ?? []).map((a) => attendee(a, "Optional"))
|
|
51885
|
-
];
|
|
51886
|
-
const tz = timeZone ?? await mailboxTimeZone(client2, { required: true }) ?? "UTC";
|
|
52754
|
+
async ({ to, cc, bcc, subject, body, html, saveToSentItems, confirmToken }, ctx) => {
|
|
51887
52755
|
const payload = {
|
|
51888
|
-
|
|
51889
|
-
|
|
51890
|
-
|
|
51891
|
-
|
|
51892
|
-
|
|
51893
|
-
|
|
51894
|
-
|
|
52756
|
+
Message: {
|
|
52757
|
+
...messageBody(subject, body, html),
|
|
52758
|
+
ToRecipients: toRecipients(to),
|
|
52759
|
+
...cc?.length ? { CcRecipients: toRecipients(cc) } : {},
|
|
52760
|
+
...bcc?.length ? { BccRecipients: toRecipients(bcc) } : {}
|
|
52761
|
+
},
|
|
52762
|
+
SaveToSentItems: saveToSentItems !== false
|
|
51895
52763
|
};
|
|
52764
|
+
const recipients = [...to ?? [], ...cc ?? [], ...bcc ?? []].join(", ");
|
|
51896
52765
|
const gate = await confirmWrite(ctx, {
|
|
51897
|
-
tool: "
|
|
51898
|
-
action: "
|
|
51899
|
-
message: "Review and confirm this
|
|
51900
|
-
summary: `
|
|
52766
|
+
tool: "outlook_send_message",
|
|
52767
|
+
action: "mail.send",
|
|
52768
|
+
message: "Review and confirm this email before it is sent:",
|
|
52769
|
+
summary: `Send mail "${subject}" to ${recipients || "(no recipients)"}`,
|
|
51901
52770
|
// One signed-in mailbox per server process.
|
|
51902
52771
|
account: void 0,
|
|
51903
|
-
request: { method: "POST", path: "/me/
|
|
52772
|
+
request: { method: "POST", path: "/me/sendmail", body: payload },
|
|
51904
52773
|
confirmToken
|
|
51905
52774
|
});
|
|
51906
|
-
if (gate) return gate;
|
|
51907
|
-
const
|
|
51908
|
-
|
|
51909
|
-
|
|
51910
|
-
);
|
|
51911
|
-
if (!write.ok) return write.result;
|
|
51912
|
-
const created = write.value;
|
|
51913
|
-
let joinUrl = joinUrlOf(created);
|
|
51914
|
-
if (wantTeams && !joinUrl && created?.Id) {
|
|
51915
|
-
const after = await client2.get(
|
|
51916
|
-
`/me/events/${encodeURIComponent(created.Id)}?$select=OnlineMeeting`
|
|
51917
|
-
);
|
|
51918
|
-
joinUrl = joinUrlOf(after);
|
|
51919
|
-
}
|
|
51920
|
-
return minifiedResult({
|
|
51921
|
-
created: true,
|
|
51922
|
-
Id: created?.Id,
|
|
51923
|
-
WebLink: created?.WebLink,
|
|
51924
|
-
TimeZone: tz,
|
|
51925
|
-
...joinUrl ? { JoinUrl: joinUrl } : {},
|
|
51926
|
-
...wantTeams && !joinUrl ? { warning: "The event was created, but Outlook returned no Teams join link." } : {}
|
|
51927
|
-
});
|
|
51928
|
-
}
|
|
51929
|
-
);
|
|
51930
|
-
server.registerTool(
|
|
51931
|
-
"outlook_update_event",
|
|
51932
|
-
{
|
|
51933
|
-
description: "Update a meeting you organize: retitle, move, relocate, edit the description, or add/remove attendees. Moving only `start` keeps the meeting's length. It does not add a Microsoft Teams meeting unless you pass `teamsMeeting: true` (on an event that lacks one). Attendees are sent an updated invitation. Only the organizer can update a meeting \u2014 an attendee's edit would change their own copy alone, so it is refused. The result is verified by re-reading the event. " + CONFIRM_FLOW_SENTENCE + " " + CONFIRM_INJECTION_RULE,
|
|
51934
|
-
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
|
|
51935
|
-
_meta: workMeta("change", { aliases: ["reschedule", "move the meeting"] }),
|
|
51936
|
-
inputSchema: external_exports.object({
|
|
51937
|
-
id: external_exports.string().min(1).describe("Event Id from outlook_list_events or outlook_get_event"),
|
|
51938
|
-
subject: external_exports.string().optional().describe("New title"),
|
|
51939
|
-
start: external_exports.string().optional().describe("New start, ISO 8601 local time e.g. 2026-09-22T15:00:00"),
|
|
51940
|
-
end: external_exports.string().optional().describe("New end. Omit when moving `start` to keep the current length."),
|
|
51941
|
-
timeZone: external_exports.string().optional().describe("Windows time-zone name for start/end. Defaults to the MAILBOX time zone."),
|
|
51942
|
-
location: external_exports.string().optional().describe("New location display name"),
|
|
51943
|
-
body: external_exports.string().optional().describe("New description (replaces the existing one)"),
|
|
51944
|
-
addAttendees: recipientList.describe("Required attendees to add"),
|
|
51945
|
-
addOptionalAttendees: recipientList.describe("Optional attendees to add"),
|
|
51946
|
-
removeAttendees: recipientList.describe("Attendees to remove"),
|
|
51947
|
-
teamsMeeting: addTeamsMeetingParam,
|
|
51948
|
-
confirmToken: confirmTokenParam
|
|
51949
|
-
})
|
|
51950
|
-
},
|
|
51951
|
-
async (args, ctx) => {
|
|
51952
|
-
const tz = args.timeZone ?? await mailboxTimeZone(client2, { required: true }) ?? "UTC";
|
|
51953
|
-
const path = `/me/events/${encodeURIComponent(args.id)}`;
|
|
51954
|
-
const readEvent = (zone) => client2.get(`${path}?$select=${EVENT_STATE_SELECT}`, {
|
|
51955
|
-
prefer: `outlook.timezone="${zone}"`
|
|
51956
|
-
});
|
|
51957
|
-
const current = await readEvent(tz);
|
|
51958
|
-
if (current?.IsOrganizer === false) {
|
|
51959
|
-
throw new McpToolError("Only the organizer can update this meeting.", {
|
|
51960
|
-
hint: "You are an attendee. Changing your copy would not reach the organizer or anyone else; ask the organizer, or propose a new time from Outlook."
|
|
51961
|
-
});
|
|
51962
|
-
}
|
|
51963
|
-
const payload = {};
|
|
51964
|
-
if (args.subject !== void 0) payload.Subject = args.subject;
|
|
51965
|
-
if (args.location !== void 0) payload.Location = { DisplayName: args.location };
|
|
51966
|
-
if (args.body !== void 0) payload.Body = { ContentType: "Text", Content: args.body };
|
|
51967
|
-
if (args.start !== void 0 || args.end !== void 0) {
|
|
51968
|
-
const start = args.start !== void 0 ? dateTimeTimeZone2(args.start, tz) : void 0;
|
|
51969
|
-
let end = args.end !== void 0 ? dateTimeTimeZone2(args.end, tz) : void 0;
|
|
51970
|
-
if (start && !end && current?.Start?.DateTime && current?.End?.DateTime) {
|
|
51971
|
-
const length = naiveMs(current.End.DateTime) - naiveMs(current.Start.DateTime);
|
|
51972
|
-
end = { DateTime: fromNaiveMs(naiveMs(start.DateTime) + length), TimeZone: start.TimeZone };
|
|
51973
|
-
}
|
|
51974
|
-
if (args.start !== void 0 && args.end !== void 0 && start?.TimeZone !== end?.TimeZone) {
|
|
51975
|
-
throw new McpToolError("`start` and `end` are in different zones.", {
|
|
51976
|
-
hint: "Give both the same way: both local wall-clock times, or both with Z or an offset."
|
|
51977
|
-
});
|
|
51978
|
-
}
|
|
51979
|
-
const startIn = start ?? (end && end.TimeZone !== tz ? (await readEvent(end.TimeZone))?.Start : current?.Start);
|
|
51980
|
-
if (end && startIn?.DateTime && naiveMs(end.DateTime) <= naiveMs(startIn.DateTime)) {
|
|
51981
|
-
throw new McpToolError("The meeting would end before it starts.", {
|
|
51982
|
-
hint: "`end` must be later than `start`."
|
|
51983
|
-
});
|
|
51984
|
-
}
|
|
51985
|
-
if (start) payload.Start = start;
|
|
51986
|
-
if (end) payload.End = end;
|
|
51987
|
-
}
|
|
51988
|
-
const adding = [
|
|
51989
|
-
...(args.addAttendees ?? []).map((a) => attendee(a, "Required")),
|
|
51990
|
-
...(args.addOptionalAttendees ?? []).map((a) => attendee(a, "Optional"))
|
|
51991
|
-
];
|
|
51992
|
-
const removing = new Set((args.removeAttendees ?? []).map((a) => a.toLowerCase()));
|
|
51993
|
-
if (adding.length || removing.size) {
|
|
51994
|
-
const next = (current?.Attendees ?? []).filter((a) => !removing.has(lower(a) ?? "")).map(({ Type, EmailAddress }) => ({ Type, EmailAddress }));
|
|
51995
|
-
for (const a of adding) {
|
|
51996
|
-
if (!next.some((n) => lower(n) === lower(a))) next.push(a);
|
|
51997
|
-
}
|
|
51998
|
-
payload.Attendees = next;
|
|
51999
|
-
}
|
|
52000
|
-
const addTeams = args.teamsMeeting === true && !(current && isTeams(current));
|
|
52001
|
-
if (addTeams) Object.assign(payload, TEAMS);
|
|
52002
|
-
if (Object.keys(payload).length === 0) {
|
|
52003
|
-
throw new McpToolError("Nothing to update.", {
|
|
52004
|
-
hint: "Pass at least one of subject, start, end, location, body, attendee changes, or teamsMeeting: true."
|
|
52005
|
-
});
|
|
52006
|
-
}
|
|
52007
|
-
const gate = await confirmWrite(ctx, {
|
|
52008
|
-
tool: "outlook_update_event",
|
|
52009
|
-
action: "calendar.update_event",
|
|
52010
|
-
message: addTeams ? "Review and confirm this change (attendees are sent an updated invitation). This will also make it a Microsoft Teams meeting: attendees get a Teams join link." : "Review and confirm this change (attendees are sent an updated invitation):",
|
|
52011
|
-
summary: `Update "${current?.Subject ?? args.id}": ${[
|
|
52012
|
-
Object.keys(payload).filter((k) => !(k in TEAMS)).join(", "),
|
|
52013
|
-
addTeams ? "this will also make it a Microsoft Teams meeting" : ""
|
|
52014
|
-
].filter((part) => part !== "").join("; ")}`,
|
|
52015
|
-
// One signed-in mailbox per server process.
|
|
52016
|
-
account: void 0,
|
|
52017
|
-
target: args.id,
|
|
52018
|
-
request: { method: "PATCH", path, body: payload },
|
|
52019
|
-
confirmToken: args.confirmToken
|
|
52020
|
-
});
|
|
52021
|
-
if (gate) return gate;
|
|
52022
|
-
await client2.write("PATCH", path, payload);
|
|
52023
|
-
const writtenZone = (payload.Start ?? payload.End)?.TimeZone ?? tz;
|
|
52024
|
-
const after = await readEvent(writtenZone);
|
|
52025
|
-
const unchanged = [];
|
|
52026
|
-
if (payload.Subject !== void 0 && after?.Subject !== payload.Subject) unchanged.push("Subject");
|
|
52027
|
-
for (const key of ["Start", "End"]) {
|
|
52028
|
-
const want = payload[key];
|
|
52029
|
-
if (want && trimTime2(after?.[key]?.DateTime) !== trimTime2(want.DateTime)) unchanged.push(key);
|
|
52030
|
-
}
|
|
52031
|
-
if (payload.Location !== void 0 && after?.Location?.DisplayName !== args.location) {
|
|
52032
|
-
unchanged.push("Location");
|
|
52033
|
-
}
|
|
52034
|
-
if (payload.Attendees !== void 0) {
|
|
52035
|
-
const want = payload.Attendees.map(lower).sort().join();
|
|
52036
|
-
const got = (after?.Attendees ?? []).map(lower).sort().join();
|
|
52037
|
-
if (want !== got) unchanged.push("Attendees");
|
|
52038
|
-
}
|
|
52039
|
-
const joinUrl = joinUrlOf(after);
|
|
52040
|
-
if (addTeams && !joinUrl) unchanged.push("Teams meeting");
|
|
52041
|
-
return minifiedResult({
|
|
52042
|
-
updated: unchanged.length === 0,
|
|
52043
|
-
Id: after?.Id ?? args.id,
|
|
52044
|
-
changed: Object.keys(payload),
|
|
52045
|
-
...joinUrl ? { JoinUrl: joinUrl } : {},
|
|
52046
|
-
...unchanged.length ? { warning: `Outlook accepted the update but these did not change: ${unchanged.join(", ")}.` } : {}
|
|
52047
|
-
});
|
|
52775
|
+
if (gate) return gate;
|
|
52776
|
+
const sent = await writeOrUnknown2(() => client2.write("POST", "/me/sendmail", payload), "Sent Items");
|
|
52777
|
+
if (!sent.ok) return sent.result;
|
|
52778
|
+
return minifiedResult({ sent: true, subject, recipients: recipients || null });
|
|
52048
52779
|
}
|
|
52049
52780
|
);
|
|
52050
52781
|
server.registerTool(
|
|
52051
|
-
"
|
|
52782
|
+
"outlook_create_draft",
|
|
52052
52783
|
{
|
|
52053
|
-
description: "
|
|
52054
|
-
|
|
52055
|
-
|
|
52784
|
+
description: "Create a draft message in the Drafts folder without sending it. Returns the created draft, which can be reviewed and sent from Outlook. It does not ask to confirm: a draft reaches nobody until it is sent from Outlook. Calling it twice leaves two drafts. " + TIMEOUT_SENTENCE,
|
|
52785
|
+
// Self-only and additive, like outlook_reply with draftOnly: nothing is
|
|
52786
|
+
// overwritten or sent. Not idempotent: a repeat creates a second draft.
|
|
52787
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
52788
|
+
_meta: workMeta("change", { aliases: ["draft", "draft an email"] }),
|
|
52056
52789
|
inputSchema: external_exports.object({
|
|
52057
|
-
|
|
52058
|
-
|
|
52059
|
-
|
|
52060
|
-
|
|
52061
|
-
|
|
52062
|
-
confirmToken: confirmTokenParam
|
|
52790
|
+
to: recipientList,
|
|
52791
|
+
cc: recipientList,
|
|
52792
|
+
subject: external_exports.string().describe("Subject line"),
|
|
52793
|
+
body: external_exports.string().describe("Message body"),
|
|
52794
|
+
html: external_exports.boolean().optional().describe("Compose the body as HTML (default: plain text)")
|
|
52063
52795
|
})
|
|
52064
52796
|
},
|
|
52065
|
-
async ({
|
|
52066
|
-
|
|
52067
|
-
|
|
52068
|
-
|
|
52069
|
-
}
|
|
52070
|
-
}
|
|
52071
|
-
|
|
52072
|
-
|
|
52073
|
-
|
|
52074
|
-
`/me/messages/${encodeURIComponent(messageId)}`,
|
|
52075
|
-
{ query: { $select: `Id,${MEETING_MESSAGE_TYPE}`, $expand: EVENT_EXPAND } }
|
|
52076
|
-
);
|
|
52077
|
-
event = resolvedEvent(msg?.Event);
|
|
52078
|
-
if (!event) {
|
|
52079
|
-
if (isMeetingMessage(msg)) {
|
|
52080
|
-
throw new McpToolError(
|
|
52081
|
-
"This invite's meeting is no longer on your calendar (already declined, cancelled or deleted) \u2014 there is nothing to respond to.",
|
|
52082
|
-
{ hint: "Mark the invite read or file it; answering it again is not possible." }
|
|
52083
|
-
);
|
|
52084
|
-
}
|
|
52085
|
-
throw new McpToolError("That message is not a meeting invite.", {
|
|
52086
|
-
hint: 'Only messages outlook_list_unread_messages marks kind "meetingRequest" carry an event to answer.'
|
|
52087
|
-
});
|
|
52088
|
-
}
|
|
52089
|
-
} else {
|
|
52090
|
-
event = await client2.get(
|
|
52091
|
-
`/me/events/${encodeURIComponent(eventId)}?$select=${INVITE_EVENT_SELECT}`
|
|
52092
|
-
);
|
|
52093
|
-
}
|
|
52094
|
-
const id = event?.Id ?? eventId;
|
|
52095
|
-
if (event?.IsCancelled === true) {
|
|
52096
|
-
throw new McpToolError("This meeting has been cancelled; there is nothing to respond to.", {
|
|
52097
|
-
hint: "Remove it from your calendar with outlook_delete_event if it is still shown."
|
|
52098
|
-
});
|
|
52099
|
-
}
|
|
52100
|
-
if (event?.IsOrganizer === true || event?.ResponseStatus?.Response === "Organizer") {
|
|
52101
|
-
throw new McpToolError("You organize this meeting, so you cannot respond to it.", {
|
|
52102
|
-
hint: "To change or call it off, use outlook_update_event or Outlook itself."
|
|
52103
|
-
});
|
|
52104
|
-
}
|
|
52105
|
-
const { verb, status } = INVITE_RESPONSES[response];
|
|
52106
|
-
const notify = sendResponse !== false;
|
|
52107
|
-
if (!notify && comment) {
|
|
52108
|
-
throw new McpToolError("A comment can only go with a response the organizer receives.", {
|
|
52109
|
-
hint: "Drop the comment, or leave sendResponse at its default (true) so the organizer gets both."
|
|
52110
|
-
});
|
|
52111
|
-
}
|
|
52112
|
-
const path = `/me/events/${encodeURIComponent(id)}/${verb}`;
|
|
52113
|
-
const payload = { ...comment ? { Comment: comment } : {}, SendResponse: notify };
|
|
52114
|
-
if (notify) {
|
|
52115
|
-
const when = `${trimTime2(event?.Start?.DateTime) ?? "?"} to ${trimTime2(event?.End?.DateTime) ?? "?"}${event?.Start?.TimeZone ? ` (${event.Start.TimeZone})` : ""}`;
|
|
52116
|
-
const organizer = event?.Organizer?.EmailAddress?.Address ?? "the organizer";
|
|
52117
|
-
const gate = await confirmWrite(ctx, {
|
|
52118
|
-
tool: "outlook_respond_to_invite",
|
|
52119
|
-
action: "calendar.respond_to_invite",
|
|
52120
|
-
message: "Review and confirm this response (the organizer is notified):",
|
|
52121
|
-
summary: `${response === "tentative" ? "Tentatively accept" : response === "accept" ? "Accept" : "Decline"} "${event?.Subject ?? id}" ${when}, organized by ${organizer}` + (comment ? `, with comment: "${comment}"` : ""),
|
|
52122
|
-
// One signed-in mailbox per server process.
|
|
52123
|
-
account: void 0,
|
|
52124
|
-
target: id,
|
|
52125
|
-
request: { method: "POST", path, body: payload },
|
|
52126
|
-
confirmToken
|
|
52127
|
-
});
|
|
52128
|
-
if (gate) return gate;
|
|
52129
|
-
}
|
|
52130
|
-
const write = await writeOrUnknown2(
|
|
52131
|
-
() => client2.write("POST", path, payload),
|
|
52132
|
-
"the event's response (outlook_get_event)"
|
|
52797
|
+
async ({ to, cc, subject, body, html }) => {
|
|
52798
|
+
const payload = {
|
|
52799
|
+
...messageBody(subject, body, html),
|
|
52800
|
+
ToRecipients: toRecipients(to),
|
|
52801
|
+
...cc?.length ? { CcRecipients: toRecipients(cc) } : {}
|
|
52802
|
+
};
|
|
52803
|
+
const create = await writeOrUnknown2(
|
|
52804
|
+
() => client2.write("POST", "/me/messages", payload),
|
|
52805
|
+
"Drafts"
|
|
52133
52806
|
);
|
|
52134
|
-
if (!
|
|
52135
|
-
|
|
52136
|
-
|
|
52137
|
-
after = await client2.get(
|
|
52138
|
-
`/me/events/${encodeURIComponent(id)}?$select=ResponseStatus`
|
|
52139
|
-
);
|
|
52140
|
-
} catch (e) {
|
|
52141
|
-
if (response === "decline" && errorStatusOf(e) === 404) {
|
|
52142
|
-
return minifiedResult({
|
|
52143
|
-
responded: true,
|
|
52144
|
-
eventId: id,
|
|
52145
|
-
response: status,
|
|
52146
|
-
sentResponse: notify,
|
|
52147
|
-
note: "The declined meeting is no longer on your calendar.",
|
|
52148
|
-
inviteMessage: INVITE_FILED_NOTE
|
|
52149
|
-
});
|
|
52150
|
-
}
|
|
52151
|
-
return minifiedResult({
|
|
52152
|
-
responded: null,
|
|
52153
|
-
eventId: id,
|
|
52154
|
-
sentResponse: notify,
|
|
52155
|
-
warning: `Outlook accepted the response but re-reading the event failed (${e instanceof Error ? e.message : String(e)}), so it is unverified.`,
|
|
52156
|
-
inviteMessage: INVITE_FILED_NOTE
|
|
52157
|
-
});
|
|
52158
|
-
}
|
|
52159
|
-
const got = after?.ResponseStatus?.Response;
|
|
52160
|
-
return minifiedResult({
|
|
52161
|
-
responded: got === status,
|
|
52162
|
-
eventId: id,
|
|
52163
|
-
response: got,
|
|
52164
|
-
sentResponse: notify,
|
|
52165
|
-
...got === status ? {} : { warning: `Outlook accepted the response but the status did not change (still ${got ?? "unknown"}).` },
|
|
52166
|
-
inviteMessage: INVITE_FILED_NOTE
|
|
52167
|
-
});
|
|
52807
|
+
if (!create.ok) return create.result;
|
|
52808
|
+
const created = create.value;
|
|
52809
|
+
return minifiedResult({ created: true, Id: created?.Id, WebLink: created?.WebLink });
|
|
52168
52810
|
}
|
|
52169
52811
|
);
|
|
52170
52812
|
server.registerTool(
|
|
52171
|
-
"
|
|
52813
|
+
"outlook_mark_read",
|
|
52172
52814
|
{
|
|
52173
|
-
description:
|
|
52174
|
-
annotations: { readOnlyHint: false, destructiveHint:
|
|
52175
|
-
_meta: workMeta("change"
|
|
52815
|
+
description: "Mark a message read or unread. The result is verified by re-reading the message \u2014 a 2xx alone is not proof it persisted. " + CONFIRM_FLOW_SENTENCE,
|
|
52816
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
52817
|
+
_meta: workMeta("change"),
|
|
52176
52818
|
inputSchema: external_exports.object({
|
|
52177
|
-
id: external_exports.string().min(1).describe("
|
|
52819
|
+
id: external_exports.string().min(1).describe("Message Id"),
|
|
52820
|
+
isRead: external_exports.boolean().describe("true to mark read, false to mark unread"),
|
|
52178
52821
|
confirmToken: confirmTokenParam
|
|
52179
52822
|
})
|
|
52180
52823
|
},
|
|
52181
|
-
async ({ id, confirmToken }, ctx) => {
|
|
52182
|
-
const path = `/me/
|
|
52183
|
-
const
|
|
52184
|
-
const
|
|
52185
|
-
|
|
52186
|
-
|
|
52187
|
-
|
|
52188
|
-
|
|
52189
|
-
|
|
52190
|
-
|
|
52191
|
-
|
|
52192
|
-
|
|
52193
|
-
|
|
52194
|
-
}
|
|
52195
|
-
|
|
52196
|
-
|
|
52197
|
-
const
|
|
52198
|
-
if (notifies) {
|
|
52199
|
-
const when = `${trimTime2(event?.Start?.DateTime) ?? "?"} to ${trimTime2(event?.End?.DateTime) ?? "?"}${event?.Start?.TimeZone ? ` (${event.Start.TimeZone})` : ""}`;
|
|
52200
|
-
const gate = await confirmWrite(ctx, {
|
|
52201
|
-
tool: "outlook_delete_event",
|
|
52202
|
-
action: "calendar.delete_event",
|
|
52203
|
-
message: "Review and confirm this deletion (every attendee is sent a cancellation):",
|
|
52204
|
-
summary: `Delete "${event?.Subject ?? id}" ${when} \u2014 ${scope}; ${attendeeCount} attendee${attendeeCount === 1 ? "" : "s"} will be sent a cancellation`,
|
|
52205
|
-
// One signed-in mailbox per server process.
|
|
52206
|
-
account: void 0,
|
|
52207
|
-
target: id,
|
|
52208
|
-
request: { method: "DELETE", path },
|
|
52209
|
-
preview: { scope, attendees: attendeeCount },
|
|
52210
|
-
confirmToken
|
|
52211
|
-
});
|
|
52212
|
-
if (gate) return gate;
|
|
52213
|
-
}
|
|
52214
|
-
const write = await writeOrUnknown2(
|
|
52215
|
-
() => client2.write("DELETE", path),
|
|
52216
|
-
"the calendar (outlook_get_event)"
|
|
52217
|
-
);
|
|
52218
|
-
if (!write.ok) return write.result;
|
|
52219
|
-
const base = { Id: id, Subject: event?.Subject, scope, cancellationSent: notifies };
|
|
52220
|
-
let after;
|
|
52221
|
-
try {
|
|
52222
|
-
after = await client2.get(`${path}?$select=Id`);
|
|
52223
|
-
} catch (e) {
|
|
52224
|
-
if (errorStatusOf(e) === 404) return minifiedResult({ deleted: true, ...base });
|
|
52225
|
-
return minifiedResult({
|
|
52226
|
-
deleted: null,
|
|
52227
|
-
...base,
|
|
52228
|
-
warning: `Outlook accepted the delete but re-reading the event failed (${e instanceof Error ? e.message : String(e)}), so it is unverified.`
|
|
52229
|
-
});
|
|
52230
|
-
}
|
|
52824
|
+
async ({ id, isRead, confirmToken }, ctx) => {
|
|
52825
|
+
const path = `/me/messages/${encodeURIComponent(id)}`;
|
|
52826
|
+
const payload = { IsRead: isRead };
|
|
52827
|
+
const gate = await confirmWrite(ctx, {
|
|
52828
|
+
tool: "outlook_mark_read",
|
|
52829
|
+
action: "mail.mark_read",
|
|
52830
|
+
message: "Review and confirm this change:",
|
|
52831
|
+
summary: `Mark message ${id} as ${isRead ? "read" : "unread"}`,
|
|
52832
|
+
// One signed-in mailbox per server process.
|
|
52833
|
+
account: void 0,
|
|
52834
|
+
target: id,
|
|
52835
|
+
request: { method: "PATCH", path, body: payload },
|
|
52836
|
+
confirmToken
|
|
52837
|
+
});
|
|
52838
|
+
if (gate) return gate;
|
|
52839
|
+
await client2.write("PATCH", path, payload);
|
|
52840
|
+
const after = await client2.get(`${path}?$select=IsRead`);
|
|
52231
52841
|
return minifiedResult({
|
|
52232
|
-
|
|
52233
|
-
|
|
52234
|
-
warning:
|
|
52842
|
+
updated: after?.IsRead === isRead,
|
|
52843
|
+
IsRead: after?.IsRead,
|
|
52844
|
+
...after?.IsRead === isRead ? {} : { warning: "Outlook accepted the write but the value did not change." }
|
|
52235
52845
|
});
|
|
52236
52846
|
}
|
|
52237
52847
|
);
|
|
52238
52848
|
server.registerTool(
|
|
52239
|
-
"
|
|
52849
|
+
"outlook_update_message",
|
|
52240
52850
|
{
|
|
52241
|
-
description:
|
|
52242
|
-
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
|
|
52243
|
-
_meta: workMeta("
|
|
52851
|
+
description: 'Flag, categorise and/or mark read up to 50 messages at once \u2014 the filing step of the triage loop (outlook_list_unread_messages \u2192 act \u2192 update). `flag`: "flagged", "complete" or "none". `categories` replaces a message\'s categories; `addCategories`/`removeCategories` edit each message\'s current list instead (names from outlook_list_categories). Duplicate ids are collapsed, so each message is written once. Each id gets its own result `{ id, ok, error? }`, verified against what Outlook stored; one failure does not stop the rest, but a rejected or missing credential fails the whole call. Only your mailbox changes \u2014 nobody is notified. ' + CONFIRM_FLOW_SENTENCE,
|
|
52852
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
52853
|
+
_meta: workMeta("change", { aliases: ["flag", "categorize"] }),
|
|
52244
52854
|
inputSchema: external_exports.object({
|
|
52245
|
-
|
|
52246
|
-
|
|
52247
|
-
|
|
52248
|
-
|
|
52249
|
-
|
|
52855
|
+
messageIds: external_exports.array(external_exports.string().min(1)).min(1).max(50).describe("Message Ids (1 to 50)"),
|
|
52856
|
+
flag: external_exports.enum(["flagged", "complete", "none"]).optional().describe("Follow-up flag to set"),
|
|
52857
|
+
categories: external_exports.array(external_exports.string().min(1)).optional().describe("Replace the categories with exactly these ([] clears them)"),
|
|
52858
|
+
addCategories: external_exports.array(external_exports.string().min(1)).optional().describe("Categories to add"),
|
|
52859
|
+
removeCategories: external_exports.array(external_exports.string().min(1)).optional().describe("Categories to remove"),
|
|
52860
|
+
isRead: external_exports.boolean().optional().describe("true to mark read, false to mark unread"),
|
|
52250
52861
|
confirmToken: confirmTokenParam
|
|
52251
52862
|
})
|
|
52252
52863
|
},
|
|
52253
|
-
async ({
|
|
52254
|
-
|
|
52255
|
-
|
|
52256
|
-
|
|
52257
|
-
|
|
52258
|
-
|
|
52259
|
-
|
|
52260
|
-
throw new McpToolError("`to` applies only to a forward.", {
|
|
52261
|
-
hint: 'A reply goes where Outlook computes; use mode "forward" to send it somewhere else, or outlook_send_message.'
|
|
52864
|
+
async ({ messageIds, flag, categories, addCategories, removeCategories, isRead, confirmToken }, ctx) => {
|
|
52865
|
+
const add = addCategories ?? [];
|
|
52866
|
+
const remove = removeCategories ?? [];
|
|
52867
|
+
const editing = add.length > 0 || remove.length > 0;
|
|
52868
|
+
if (categories !== void 0 && editing) {
|
|
52869
|
+
throw new McpToolError("Pass `categories` or `addCategories`/`removeCategories`, not both.", {
|
|
52870
|
+
hint: "`categories` replaces the whole list; the add/remove pair edits the current one."
|
|
52262
52871
|
});
|
|
52263
52872
|
}
|
|
52264
|
-
const
|
|
52265
|
-
|
|
52266
|
-
|
|
52267
|
-
|
|
52268
|
-
|
|
52269
|
-
|
|
52270
|
-
|
|
52271
|
-
|
|
52272
|
-
|
|
52273
|
-
),
|
|
52274
|
-
"Drafts"
|
|
52275
|
-
);
|
|
52276
|
-
if (!create.ok) return create.result;
|
|
52277
|
-
const created = create.value;
|
|
52278
|
-
if (!created?.Id) {
|
|
52279
|
-
throw new McpToolError("Outlook did not return the draft it created.", {
|
|
52280
|
-
hint: "Check the Drafts folder before trying again."
|
|
52281
|
-
});
|
|
52282
|
-
}
|
|
52283
|
-
try {
|
|
52284
|
-
await client2.write("PATCH", `/me/messages/${encodeURIComponent(created.Id)}`, {
|
|
52285
|
-
Body: bodyWithComment(created.Body, comment),
|
|
52286
|
-
...forwardTo
|
|
52287
|
-
});
|
|
52288
|
-
} catch (e) {
|
|
52289
|
-
const what = `your comment${mode === "forward" ? " and recipients" : ""}`;
|
|
52290
|
-
const reason = e instanceof Error ? e.message : String(e);
|
|
52291
|
-
if (isCredentialFailure(e)) {
|
|
52292
|
-
throw new McpToolError(
|
|
52293
|
-
`The reply draft was created (draftId ${created.Id}) but adding ${what} failed: ${reason}`,
|
|
52294
|
-
{
|
|
52295
|
-
hint: `${e instanceof McpToolError && e.hint ? `${e.hint} ` : ""}Then edit draft ${created.Id} in Drafts, or delete it before trying again.`,
|
|
52296
|
-
cause: e,
|
|
52297
|
-
kind: errorKindOf(e),
|
|
52298
|
-
status: errorStatusOf(e)
|
|
52299
|
-
}
|
|
52300
|
-
);
|
|
52301
|
-
}
|
|
52302
|
-
const warning = e instanceof WriteOutcomeUnknownError ? `The draft was created, and ${what} may have been applied: Outlook did not confirm the edit (${e.timedOut ? "it did not answer in time" : "the connection dropped"}). Check the draft in Drafts before editing it again or trying again.` : `The draft was created but adding ${what} failed (${reason}). Edit it in Drafts, or delete it before trying again.`;
|
|
52303
|
-
return minifiedResult({
|
|
52304
|
-
drafted: true,
|
|
52305
|
-
mode,
|
|
52306
|
-
draftId: created.Id,
|
|
52307
|
-
WebLink: created.WebLink,
|
|
52308
|
-
warning
|
|
52309
|
-
});
|
|
52310
|
-
}
|
|
52311
|
-
return minifiedResult({ drafted: true, mode, draftId: created.Id, WebLink: created.WebLink });
|
|
52873
|
+
const fixed = {
|
|
52874
|
+
...isRead !== void 0 ? { IsRead: isRead } : {},
|
|
52875
|
+
...flag ? { Flag: { FlagStatus: FLAG_STATUS[flag] } } : {},
|
|
52876
|
+
...categories !== void 0 ? { Categories: categories } : {}
|
|
52877
|
+
};
|
|
52878
|
+
if (Object.keys(fixed).length === 0 && !editing) {
|
|
52879
|
+
throw new McpToolError("Nothing to update.", {
|
|
52880
|
+
hint: "Pass at least one of flag, isRead, categories, addCategories or removeCategories."
|
|
52881
|
+
});
|
|
52312
52882
|
}
|
|
52313
|
-
const
|
|
52314
|
-
|
|
52315
|
-
|
|
52316
|
-
|
|
52317
|
-
|
|
52318
|
-
|
|
52319
|
-
|
|
52320
|
-
|
|
52321
|
-
|
|
52322
|
-
const who = everyone.length > MAX_RECIPIENTS ? `${everyone.slice(0, MAX_RECIPIENTS).join(", ")} and ${(everyone.length - MAX_RECIPIENTS).toLocaleString("en-US")} more` : everyone.join(", ");
|
|
52883
|
+
const ids = [...new Set(messageIds)];
|
|
52884
|
+
const pathOf = (id) => `/me/messages/${encodeURIComponent(id)}`;
|
|
52885
|
+
const changes = [
|
|
52886
|
+
...flag ? [`flag ${flag}`] : [],
|
|
52887
|
+
...isRead !== void 0 ? [isRead ? "mark read" : "mark unread"] : [],
|
|
52888
|
+
...categories !== void 0 ? [`set categories [${categories.join(", ")}]`] : [],
|
|
52889
|
+
...add.length ? [`add categories [${add.join(", ")}]`] : [],
|
|
52890
|
+
...remove.length ? [`remove categories [${remove.join(", ")}]`] : []
|
|
52891
|
+
];
|
|
52323
52892
|
const gate = await confirmWrite(ctx, {
|
|
52324
|
-
tool: "
|
|
52325
|
-
action:
|
|
52326
|
-
message: "Review and confirm this
|
|
52327
|
-
summary:
|
|
52893
|
+
tool: "outlook_update_message",
|
|
52894
|
+
action: "mail.update",
|
|
52895
|
+
message: "Review and confirm this change:",
|
|
52896
|
+
summary: `Update ${ids.length} message${ids.length === 1 ? "" : "s"}: ${changes.join("; ")}`,
|
|
52328
52897
|
// One signed-in mailbox per server process.
|
|
52329
52898
|
account: void 0,
|
|
52330
|
-
target:
|
|
52331
|
-
request: {
|
|
52899
|
+
target: ids.join(","),
|
|
52900
|
+
request: {
|
|
52901
|
+
method: "PATCH",
|
|
52902
|
+
path: ids.length === 1 ? pathOf(ids[0]) : "/me/messages/{id}",
|
|
52903
|
+
body: editing ? { ...fixed, Categories: `(each message's current categories${add.length ? ` + ${add.join(", ")}` : ""}${remove.length ? ` - ${remove.join(", ")}` : ""})` } : fixed
|
|
52904
|
+
},
|
|
52332
52905
|
confirmToken
|
|
52333
52906
|
});
|
|
52334
52907
|
if (gate) return gate;
|
|
52335
|
-
const
|
|
52336
|
-
|
|
52337
|
-
|
|
52338
|
-
|
|
52339
|
-
|
|
52340
|
-
|
|
52341
|
-
|
|
52342
|
-
|
|
52908
|
+
const results = await mapWithConcurrency(ids, UPDATE_CONCURRENCY, async (id) => {
|
|
52909
|
+
try {
|
|
52910
|
+
const payload = { ...fixed };
|
|
52911
|
+
if (editing) {
|
|
52912
|
+
const current = await client2.get(`${pathOf(id)}?$select=Categories`);
|
|
52913
|
+
payload.Categories = editCategories(current?.Categories, add, remove);
|
|
52914
|
+
}
|
|
52915
|
+
const echoed = await client2.write("PATCH", pathOf(id), payload);
|
|
52916
|
+
const after = echoed && typeof echoed === "object" ? echoed : await client2.get(`${pathOf(id)}?$select=IsRead,Flag,Categories`) ?? {};
|
|
52917
|
+
const unchanged = unchangedFields(payload, after);
|
|
52918
|
+
return unchanged.length ? { id, ok: false, error: `Outlook accepted the write but these did not change: ${unchanged.join(", ")}.` } : { id, ok: true };
|
|
52919
|
+
} catch (e) {
|
|
52920
|
+
if (isCredentialFailure(e)) throw e;
|
|
52921
|
+
return { id, ok: false, error: e instanceof Error ? e.message : String(e) };
|
|
52922
|
+
}
|
|
52343
52923
|
});
|
|
52924
|
+
const updated = results.filter((r) => r.ok).length;
|
|
52925
|
+
return minifiedResult({ updated, failed: results.length - updated, results });
|
|
52344
52926
|
}
|
|
52345
52927
|
);
|
|
52346
|
-
|
|
52347
|
-
|
|
52348
|
-
|
|
52349
|
-
|
|
52350
|
-
|
|
52351
|
-
|
|
52352
|
-
|
|
52353
|
-
|
|
52354
|
-
|
|
52355
|
-
|
|
52356
|
-
|
|
52357
|
-
|
|
52358
|
-
|
|
52359
|
-
|
|
52360
|
-
|
|
52361
|
-
|
|
52362
|
-
|
|
52363
|
-
|
|
52364
|
-
|
|
52365
|
-
|
|
52366
|
-
|
|
52367
|
-
|
|
52368
|
-
|
|
52369
|
-
|
|
52370
|
-
|
|
52371
|
-
}
|
|
52372
|
-
};
|
|
52373
|
-
},
|
|
52374
|
-
probeFn: async () => {
|
|
52375
|
-
const me = await client2.get("/me");
|
|
52376
|
-
return `authenticated as ${me?.EmailAddress ?? "unknown"}`;
|
|
52377
|
-
}
|
|
52378
|
-
});
|
|
52379
|
-
}
|
|
52380
|
-
|
|
52381
|
-
// src/tools/export.ts
|
|
52382
|
-
init_zod();
|
|
52383
|
-
init_dist2();
|
|
52384
|
-
import { createHash as createHash4 } from "node:crypto";
|
|
52385
|
-
import { readdirSync as readdirSync2, readFileSync as readFileSync4, rmdirSync, unlinkSync as unlinkSync5 } from "node:fs";
|
|
52386
|
-
import { join as join11 } from "node:path";
|
|
52387
|
-
|
|
52388
|
-
// node_modules/@chrischall/mcp-utils/dist/export/blob.js
|
|
52389
|
-
init_config();
|
|
52390
|
-
init_errors3();
|
|
52391
|
-
import { createHmac as createHmac4 } from "node:crypto";
|
|
52392
|
-
var BLOB_DEFAULT_TTL_MS = 60 * 60 * 1e3;
|
|
52393
|
-
var BLOB_MAX_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
52394
|
-
var MAX_KEY_LENGTH = 1024;
|
|
52395
|
-
var SAFE_SEGMENT = /^[A-Za-z0-9._~-]+$/;
|
|
52396
|
-
function configError(reason) {
|
|
52397
|
-
return new McpToolError(`MCP_BLOB_BASE_URL is not usable: ${reason}.`, {
|
|
52398
|
-
hint: "It should look like https://<host>/b/<registrationId> \u2014 check the deployment's environment.",
|
|
52399
|
-
kind: "unknown"
|
|
52400
|
-
});
|
|
52401
|
-
}
|
|
52402
|
-
function parseBase(baseUrl) {
|
|
52403
|
-
let parsed;
|
|
52404
|
-
try {
|
|
52405
|
-
parsed = new URL(baseUrl);
|
|
52406
|
-
} catch {
|
|
52407
|
-
throw configError("not a valid URL");
|
|
52408
|
-
}
|
|
52409
|
-
if (parsed.protocol !== "https:" && parsed.protocol !== "http:")
|
|
52410
|
-
throw configError("scheme must be http(s)");
|
|
52411
|
-
if (parsed.username || parsed.password)
|
|
52412
|
-
throw configError("must not contain credentials");
|
|
52413
|
-
if (parsed.search || parsed.hash)
|
|
52414
|
-
throw configError("must not contain a query or fragment");
|
|
52415
|
-
const path = parsed.pathname.replace(/^\/+|\/+$/g, "");
|
|
52416
|
-
const prefix = path.startsWith("b/") ? path.slice(2) : path;
|
|
52417
|
-
if (!prefix || prefix === "b")
|
|
52418
|
-
throw configError("has no registration path");
|
|
52419
|
-
return { base: `${parsed.origin}/${path}`, prefix };
|
|
52420
|
-
}
|
|
52421
|
-
function badInput(message) {
|
|
52422
|
-
return new McpToolError(message, { kind: "unknown" });
|
|
52423
|
-
}
|
|
52424
|
-
function encodeKey(key) {
|
|
52425
|
-
if (typeof key !== "string" || key.length === 0)
|
|
52426
|
-
throw badInput("Blob key must be a non-empty string.");
|
|
52427
|
-
if (key.length > MAX_KEY_LENGTH)
|
|
52428
|
-
throw badInput(`Blob key is longer than ${MAX_KEY_LENGTH} characters.`);
|
|
52429
|
-
const segments = key.split("/");
|
|
52430
|
-
for (const seg of segments) {
|
|
52431
|
-
if (seg === "" || seg === "." || seg === ".." || !SAFE_SEGMENT.test(seg)) {
|
|
52432
|
-
throw badInput('Blob key must be relative, slash-separated segments of [A-Za-z0-9._~-] with no empty, "." or ".." segment.');
|
|
52433
|
-
}
|
|
52434
|
-
}
|
|
52435
|
-
return segments.map(encodeURIComponent).join("/");
|
|
52436
|
-
}
|
|
52437
|
-
function checkContentType(contentType) {
|
|
52438
|
-
if (typeof contentType !== "string" || contentType.length === 0 || /[\x00-\x1f\x7f]/.test(contentType)) {
|
|
52439
|
-
throw badInput("Blob content type must be a non-empty string without control characters.");
|
|
52440
|
-
}
|
|
52441
|
-
}
|
|
52442
|
-
function expiry(opts) {
|
|
52443
|
-
const ttl = opts?.ttlMs ?? BLOB_DEFAULT_TTL_MS;
|
|
52444
|
-
if (!Number.isFinite(ttl) || ttl <= 0 || ttl > BLOB_MAX_TTL_MS) {
|
|
52445
|
-
throw badInput(`Blob link TTL must be between 1 ms and ${BLOB_MAX_TTL_MS} ms (24h); the gateway refuses longer.`);
|
|
52446
|
-
}
|
|
52447
|
-
return Math.floor((opts?.now ?? Date.now()) + ttl);
|
|
52448
|
-
}
|
|
52449
|
-
function toArrayBuffer(view) {
|
|
52450
|
-
const buf = view.buffer;
|
|
52451
|
-
if (buf instanceof ArrayBuffer && view.byteOffset === 0 && view.byteLength === buf.byteLength)
|
|
52452
|
-
return buf;
|
|
52453
|
-
return view.slice().buffer;
|
|
52454
|
-
}
|
|
52455
|
-
function createBlobStore(opts) {
|
|
52456
|
-
const { base, prefix } = parseBase(opts.baseUrl);
|
|
52457
|
-
const signingKey = opts.signingKey;
|
|
52458
|
-
const sign2 = (payload) => createHmac4("sha256", signingKey).update(payload).digest("base64url");
|
|
52459
|
-
const link2 = (encodedKey, exp, sig) => `${base}/${encodedKey}?exp=${exp}&sig=${encodeURIComponent(sig)}`;
|
|
52460
|
-
const signGetUrl = (key, o) => {
|
|
52461
|
-
const encoded = encodeKey(key);
|
|
52462
|
-
const exp = expiry(o);
|
|
52463
|
-
return { url: link2(encoded, exp, sign2(`${prefix}/${key}
|
|
52464
|
-
${exp}`)), expiresAtMs: exp };
|
|
52465
|
-
};
|
|
52466
|
-
const signPutUrl = (key, contentType, o) => {
|
|
52467
|
-
const encoded = encodeKey(key);
|
|
52468
|
-
checkContentType(contentType);
|
|
52469
|
-
const exp = expiry(o);
|
|
52470
|
-
return { url: link2(encoded, exp, sign2(`put\0${prefix}/${key}\0${contentType}\0${exp}`)), expiresAtMs: exp };
|
|
52471
|
-
};
|
|
52472
|
-
const put = async (key, body, contentType, o = {}) => {
|
|
52473
|
-
const { url: url2 } = signPutUrl(key, contentType, o);
|
|
52474
|
-
const bytes = typeof body === "string" ? new TextEncoder().encode(body) : body;
|
|
52475
|
-
const doFetch = o.fetchImpl ?? fetch;
|
|
52476
|
-
let res;
|
|
52477
|
-
try {
|
|
52478
|
-
res = await doFetch(url2, {
|
|
52479
|
-
method: "PUT",
|
|
52480
|
-
headers: { "content-type": contentType },
|
|
52481
|
-
body: toArrayBuffer(bytes),
|
|
52482
|
-
...o.signal ? { signal: o.signal } : {}
|
|
52928
|
+
server.registerTool(
|
|
52929
|
+
"outlook_move_message",
|
|
52930
|
+
{
|
|
52931
|
+
description: 'Move a message to another folder (e.g. "archive", "deleteditems", or a folder id from outlook_list_folders). Moving assigns a NEW message id, which is returned. ' + CONFIRM_FLOW_SENTENCE,
|
|
52932
|
+
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
52933
|
+
_meta: workMeta("change", { aliases: ["file", "archive"] }),
|
|
52934
|
+
inputSchema: external_exports.object({
|
|
52935
|
+
id: external_exports.string().min(1).describe("Message Id"),
|
|
52936
|
+
destination: external_exports.string().min(1).describe('Destination folder id or well-known name (e.g. "archive")'),
|
|
52937
|
+
confirmToken: confirmTokenParam
|
|
52938
|
+
})
|
|
52939
|
+
},
|
|
52940
|
+
async ({ id, destination, confirmToken }, ctx) => {
|
|
52941
|
+
const path = `/me/messages/${encodeURIComponent(id)}/move`;
|
|
52942
|
+
const payload = { DestinationId: destination };
|
|
52943
|
+
const gate = await confirmWrite(ctx, {
|
|
52944
|
+
tool: "outlook_move_message",
|
|
52945
|
+
action: "mail.move",
|
|
52946
|
+
message: "Review and confirm this move:",
|
|
52947
|
+
summary: `Move message ${id} to ${destination}`,
|
|
52948
|
+
// One signed-in mailbox per server process.
|
|
52949
|
+
account: void 0,
|
|
52950
|
+
target: id,
|
|
52951
|
+
request: { method: "POST", path, body: payload },
|
|
52952
|
+
confirmToken
|
|
52483
52953
|
});
|
|
52484
|
-
|
|
52485
|
-
|
|
52486
|
-
|
|
52487
|
-
|
|
52488
|
-
|
|
52954
|
+
if (gate) return gate;
|
|
52955
|
+
const moved = await client2.write(
|
|
52956
|
+
"POST",
|
|
52957
|
+
path,
|
|
52958
|
+
payload
|
|
52959
|
+
);
|
|
52960
|
+
return minifiedResult({
|
|
52961
|
+
moved: true,
|
|
52962
|
+
newId: moved?.Id,
|
|
52963
|
+
ParentFolderId: moved?.ParentFolderId,
|
|
52964
|
+
note: "The message has a new Id after a move; the old one no longer resolves."
|
|
52489
52965
|
});
|
|
52490
52966
|
}
|
|
52491
|
-
|
|
52492
|
-
|
|
52493
|
-
|
|
52494
|
-
|
|
52967
|
+
);
|
|
52968
|
+
server.registerTool(
|
|
52969
|
+
"outlook_create_event",
|
|
52970
|
+
{
|
|
52971
|
+
description: 'Create a calendar event or meeting. A Microsoft Teams meeting is attached by default (`teamsMeeting: false` to skip) and its join link is returned. `timeZone` takes a WINDOWS zone name such as "Eastern Standard Time", not an IANA name. Attendees are emailed an invitation. To pick a time first, use outlook_find_meeting_times: each suggestion it returns carries `createEventArgs` that can be passed here as-is (add `subject` if the search did not set one). ' + CONFIRM_FLOW_SENTENCE + " " + TIMEOUT_SENTENCE + " " + CONFIRM_INJECTION_RULE,
|
|
52972
|
+
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
|
|
52973
|
+
_meta: workMeta("change", { aliases: ["book a meeting", "schedule a meeting", "invite"] }),
|
|
52974
|
+
inputSchema: createEventInput
|
|
52975
|
+
},
|
|
52976
|
+
async ({ subject, start, end, timeZone, location, body, attendees, optionalAttendees, teamsMeeting, confirmToken }, ctx) => {
|
|
52977
|
+
const wantTeams = teamsMeeting !== false;
|
|
52978
|
+
const allAttendees = [
|
|
52979
|
+
...(attendees ?? []).map((a) => attendee(a, "Required")),
|
|
52980
|
+
...(optionalAttendees ?? []).map((a) => attendee(a, "Optional"))
|
|
52981
|
+
];
|
|
52982
|
+
const tz = timeZone ?? await mailboxTimeZone(client2, { required: true }) ?? "UTC";
|
|
52983
|
+
const payload = {
|
|
52984
|
+
Subject: subject,
|
|
52985
|
+
Start: { DateTime: start, TimeZone: tz },
|
|
52986
|
+
End: { DateTime: end, TimeZone: tz },
|
|
52987
|
+
...location ? { Location: { DisplayName: location } } : {},
|
|
52988
|
+
...body ? { Body: { ContentType: "Text", Content: body } } : {},
|
|
52989
|
+
...allAttendees.length ? { Attendees: allAttendees } : {},
|
|
52990
|
+
...wantTeams ? TEAMS : {}
|
|
52991
|
+
};
|
|
52992
|
+
const gate = await confirmWrite(ctx, {
|
|
52993
|
+
tool: "outlook_create_event",
|
|
52994
|
+
action: "calendar.create_event",
|
|
52995
|
+
message: "Review and confirm this event (attendees are emailed an invitation):",
|
|
52996
|
+
summary: `Create ${wantTeams ? "Teams meeting" : "event"} "${subject}" ${start} to ${end} (${tz})`,
|
|
52997
|
+
// One signed-in mailbox per server process.
|
|
52998
|
+
account: void 0,
|
|
52999
|
+
request: { method: "POST", path: "/me/events", body: payload },
|
|
53000
|
+
confirmToken
|
|
53001
|
+
});
|
|
53002
|
+
if (gate) return gate;
|
|
53003
|
+
const write = await writeOrUnknown2(
|
|
53004
|
+
() => client2.write("POST", "/me/events", payload),
|
|
53005
|
+
"the calendar (outlook_list_events)"
|
|
53006
|
+
);
|
|
53007
|
+
if (!write.ok) return write.result;
|
|
53008
|
+
const created = write.value;
|
|
53009
|
+
let joinUrl = joinUrlOf(created);
|
|
53010
|
+
if (wantTeams && !joinUrl && created?.Id) {
|
|
53011
|
+
const after = await client2.get(
|
|
53012
|
+
`/me/events/${encodeURIComponent(created.Id)}?$select=OnlineMeeting`
|
|
53013
|
+
);
|
|
53014
|
+
joinUrl = joinUrlOf(after);
|
|
53015
|
+
}
|
|
53016
|
+
return minifiedResult({
|
|
53017
|
+
created: true,
|
|
53018
|
+
Id: created?.Id,
|
|
53019
|
+
WebLink: created?.WebLink,
|
|
53020
|
+
TimeZone: tz,
|
|
53021
|
+
...joinUrl ? { JoinUrl: joinUrl } : {},
|
|
53022
|
+
...wantTeams && !joinUrl ? { warning: "The event was created, but Outlook returned no Teams join link." } : {}
|
|
52495
53023
|
});
|
|
52496
53024
|
}
|
|
52497
|
-
|
|
52498
|
-
|
|
52499
|
-
|
|
52500
|
-
|
|
52501
|
-
|
|
52502
|
-
|
|
52503
|
-
|
|
52504
|
-
|
|
52505
|
-
|
|
52506
|
-
|
|
52507
|
-
|
|
52508
|
-
|
|
52509
|
-
|
|
52510
|
-
|
|
52511
|
-
|
|
52512
|
-
|
|
52513
|
-
|
|
52514
|
-
|
|
52515
|
-
|
|
52516
|
-
|
|
52517
|
-
|
|
52518
|
-
|
|
52519
|
-
|
|
52520
|
-
|
|
52521
|
-
|
|
52522
|
-
|
|
52523
|
-
|
|
52524
|
-
|
|
52525
|
-
|
|
52526
|
-
|
|
52527
|
-
|
|
52528
|
-
|
|
52529
|
-
|
|
52530
|
-
|
|
52531
|
-
|
|
52532
|
-
|
|
52533
|
-
|
|
52534
|
-
|
|
52535
|
-
|
|
52536
|
-
|
|
52537
|
-
|
|
52538
|
-
|
|
52539
|
-
|
|
52540
|
-
|
|
52541
|
-
|
|
52542
|
-
|
|
52543
|
-
|
|
52544
|
-
|
|
52545
|
-
|
|
52546
|
-
|
|
52547
|
-
|
|
52548
|
-
|
|
52549
|
-
|
|
52550
|
-
|
|
52551
|
-
|
|
52552
|
-
|
|
52553
|
-
|
|
52554
|
-
|
|
52555
|
-
|
|
52556
|
-
|
|
52557
|
-
|
|
52558
|
-
|
|
52559
|
-
|
|
52560
|
-
]);
|
|
52561
|
-
|
|
52562
|
-
|
|
52563
|
-
|
|
52564
|
-
|
|
52565
|
-
|
|
52566
|
-
|
|
52567
|
-
}
|
|
52568
|
-
|
|
52569
|
-
|
|
52570
|
-
|
|
52571
|
-
|
|
52572
|
-
|
|
52573
|
-
|
|
52574
|
-
|
|
52575
|
-
|
|
52576
|
-
|
|
52577
|
-
|
|
52578
|
-
|
|
52579
|
-
|
|
52580
|
-
|
|
52581
|
-
|
|
52582
|
-
|
|
52583
|
-
|
|
52584
|
-
|
|
52585
|
-
|
|
52586
|
-
|
|
52587
|
-
|
|
52588
|
-
|
|
52589
|
-
|
|
52590
|
-
|
|
52591
|
-
|
|
52592
|
-
|
|
52593
|
-
|
|
52594
|
-
|
|
52595
|
-
|
|
52596
|
-
|
|
52597
|
-
|
|
52598
|
-
|
|
52599
|
-
|
|
52600
|
-
|
|
52601
|
-
if (entry.compress !== false && size > 0) {
|
|
52602
|
-
const deflated = new Uint8Array(deflateRawSync(entry.bytes));
|
|
52603
|
-
if (deflated.byteLength < size) {
|
|
52604
|
-
method = METHOD_DEFLATE;
|
|
52605
|
-
data = deflated;
|
|
53025
|
+
);
|
|
53026
|
+
server.registerTool(
|
|
53027
|
+
"outlook_update_event",
|
|
53028
|
+
{
|
|
53029
|
+
description: "Update a meeting you organize: retitle, move, relocate, edit the description, or add/remove attendees. Moving only `start` keeps the meeting's length. It does not add a Microsoft Teams meeting unless you pass `teamsMeeting: true` (on an event that lacks one). Attendees are sent an updated invitation. Only the organizer can update a meeting \u2014 an attendee's edit would change their own copy alone, so it is refused. The result is verified by re-reading the event. " + CONFIRM_FLOW_SENTENCE + " " + CONFIRM_INJECTION_RULE,
|
|
53030
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
|
|
53031
|
+
_meta: workMeta("change", { aliases: ["reschedule", "move the meeting"] }),
|
|
53032
|
+
inputSchema: external_exports.object({
|
|
53033
|
+
id: external_exports.string().min(1).describe("Event Id from outlook_list_events or outlook_get_event"),
|
|
53034
|
+
subject: external_exports.string().optional().describe("New title"),
|
|
53035
|
+
start: external_exports.string().optional().describe("New start, ISO 8601 local time e.g. 2026-09-22T15:00:00"),
|
|
53036
|
+
end: external_exports.string().optional().describe("New end. Omit when moving `start` to keep the current length."),
|
|
53037
|
+
timeZone: external_exports.string().optional().describe("Windows time-zone name for start/end. Defaults to the MAILBOX time zone."),
|
|
53038
|
+
location: external_exports.string().optional().describe("New location display name"),
|
|
53039
|
+
body: external_exports.string().optional().describe("New description (replaces the existing one)"),
|
|
53040
|
+
addAttendees: recipientList.describe("Required attendees to add"),
|
|
53041
|
+
addOptionalAttendees: recipientList.describe("Optional attendees to add"),
|
|
53042
|
+
removeAttendees: recipientList.describe("Attendees to remove"),
|
|
53043
|
+
teamsMeeting: addTeamsMeetingParam,
|
|
53044
|
+
confirmToken: confirmTokenParam
|
|
53045
|
+
})
|
|
53046
|
+
},
|
|
53047
|
+
async (args, ctx) => {
|
|
53048
|
+
const tz = args.timeZone ?? await mailboxTimeZone(client2, { required: true }) ?? "UTC";
|
|
53049
|
+
const path = `/me/events/${encodeURIComponent(args.id)}`;
|
|
53050
|
+
const readEvent = (zone) => client2.get(`${path}?$select=${EVENT_STATE_SELECT}`, {
|
|
53051
|
+
prefer: `outlook.timezone="${zone}"`
|
|
53052
|
+
});
|
|
53053
|
+
const current = await readEvent(tz);
|
|
53054
|
+
if (current?.IsOrganizer === false) {
|
|
53055
|
+
throw new McpToolError("Only the organizer can update this meeting.", {
|
|
53056
|
+
hint: "You are an attendee. Changing your copy would not reach the organizer or anyone else; ask the organizer, or propose a new time from Outlook."
|
|
53057
|
+
});
|
|
53058
|
+
}
|
|
53059
|
+
const payload = {};
|
|
53060
|
+
if (args.subject !== void 0) payload.Subject = args.subject;
|
|
53061
|
+
if (args.location !== void 0) payload.Location = { DisplayName: args.location };
|
|
53062
|
+
if (args.body !== void 0) payload.Body = { ContentType: "Text", Content: args.body };
|
|
53063
|
+
if (args.start !== void 0 || args.end !== void 0) {
|
|
53064
|
+
const start = args.start !== void 0 ? dateTimeTimeZone2(args.start, tz) : void 0;
|
|
53065
|
+
let end = args.end !== void 0 ? dateTimeTimeZone2(args.end, tz) : void 0;
|
|
53066
|
+
if (start && !end && current?.Start?.DateTime && current?.End?.DateTime) {
|
|
53067
|
+
const length = naiveMs(current.End.DateTime) - naiveMs(current.Start.DateTime);
|
|
53068
|
+
end = { DateTime: fromNaiveMs(naiveMs(start.DateTime) + length), TimeZone: start.TimeZone };
|
|
53069
|
+
}
|
|
53070
|
+
if (args.start !== void 0 && args.end !== void 0 && start?.TimeZone !== end?.TimeZone) {
|
|
53071
|
+
throw new McpToolError("`start` and `end` are in different zones.", {
|
|
53072
|
+
hint: "Give both the same way: both local wall-clock times, or both with Z or an offset."
|
|
53073
|
+
});
|
|
53074
|
+
}
|
|
53075
|
+
const startIn = start ?? (end && end.TimeZone !== tz ? (await readEvent(end.TimeZone))?.Start : current?.Start);
|
|
53076
|
+
if (end && startIn?.DateTime && naiveMs(end.DateTime) <= naiveMs(startIn.DateTime)) {
|
|
53077
|
+
throw new McpToolError("The meeting would end before it starts.", {
|
|
53078
|
+
hint: "`end` must be later than `start`."
|
|
53079
|
+
});
|
|
53080
|
+
}
|
|
53081
|
+
if (start) payload.Start = start;
|
|
53082
|
+
if (end) payload.End = end;
|
|
53083
|
+
}
|
|
53084
|
+
const adding = [
|
|
53085
|
+
...(args.addAttendees ?? []).map((a) => attendee(a, "Required")),
|
|
53086
|
+
...(args.addOptionalAttendees ?? []).map((a) => attendee(a, "Optional"))
|
|
53087
|
+
];
|
|
53088
|
+
const removing = new Set((args.removeAttendees ?? []).map((a) => a.toLowerCase()));
|
|
53089
|
+
if (adding.length || removing.size) {
|
|
53090
|
+
const next = (current?.Attendees ?? []).filter((a) => !removing.has(lower(a) ?? "")).map(({ Type, EmailAddress }) => ({ Type, EmailAddress }));
|
|
53091
|
+
for (const a of adding) {
|
|
53092
|
+
if (!next.some((n) => lower(n) === lower(a))) next.push(a);
|
|
53093
|
+
}
|
|
53094
|
+
payload.Attendees = next;
|
|
53095
|
+
}
|
|
53096
|
+
const addTeams = args.teamsMeeting === true && !(current && isTeams(current));
|
|
53097
|
+
if (addTeams) Object.assign(payload, TEAMS);
|
|
53098
|
+
if (Object.keys(payload).length === 0) {
|
|
53099
|
+
throw new McpToolError("Nothing to update.", {
|
|
53100
|
+
hint: "Pass at least one of subject, start, end, location, body, attendee changes, or teamsMeeting: true."
|
|
53101
|
+
});
|
|
53102
|
+
}
|
|
53103
|
+
const gate = await confirmWrite(ctx, {
|
|
53104
|
+
tool: "outlook_update_event",
|
|
53105
|
+
action: "calendar.update_event",
|
|
53106
|
+
message: addTeams ? "Review and confirm this change (attendees are sent an updated invitation). This will also make it a Microsoft Teams meeting: attendees get a Teams join link." : "Review and confirm this change (attendees are sent an updated invitation):",
|
|
53107
|
+
summary: `Update "${current?.Subject ?? args.id}": ${[
|
|
53108
|
+
Object.keys(payload).filter((k) => !(k in TEAMS)).join(", "),
|
|
53109
|
+
addTeams ? "this will also make it a Microsoft Teams meeting" : ""
|
|
53110
|
+
].filter((part) => part !== "").join("; ")}`,
|
|
53111
|
+
// One signed-in mailbox per server process.
|
|
53112
|
+
account: void 0,
|
|
53113
|
+
target: args.id,
|
|
53114
|
+
request: { method: "PATCH", path, body: payload },
|
|
53115
|
+
confirmToken: args.confirmToken
|
|
53116
|
+
});
|
|
53117
|
+
if (gate) return gate;
|
|
53118
|
+
await client2.write("PATCH", path, payload);
|
|
53119
|
+
const writtenZone = (payload.Start ?? payload.End)?.TimeZone ?? tz;
|
|
53120
|
+
const after = await readEvent(writtenZone);
|
|
53121
|
+
const unchanged = [];
|
|
53122
|
+
if (payload.Subject !== void 0 && after?.Subject !== payload.Subject) unchanged.push("Subject");
|
|
53123
|
+
for (const key of ["Start", "End"]) {
|
|
53124
|
+
const want = payload[key];
|
|
53125
|
+
if (want && trimTime2(after?.[key]?.DateTime) !== trimTime2(want.DateTime)) unchanged.push(key);
|
|
53126
|
+
}
|
|
53127
|
+
if (payload.Location !== void 0 && after?.Location?.DisplayName !== args.location) {
|
|
53128
|
+
unchanged.push("Location");
|
|
52606
53129
|
}
|
|
53130
|
+
if (payload.Attendees !== void 0) {
|
|
53131
|
+
const want = payload.Attendees.map(lower).sort().join();
|
|
53132
|
+
const got = (after?.Attendees ?? []).map(lower).sort().join();
|
|
53133
|
+
if (want !== got) unchanged.push("Attendees");
|
|
53134
|
+
}
|
|
53135
|
+
const joinUrl = joinUrlOf(after);
|
|
53136
|
+
if (addTeams && !joinUrl) unchanged.push("Teams meeting");
|
|
53137
|
+
return minifiedResult({
|
|
53138
|
+
updated: unchanged.length === 0,
|
|
53139
|
+
Id: after?.Id ?? args.id,
|
|
53140
|
+
changed: Object.keys(payload),
|
|
53141
|
+
...joinUrl ? { JoinUrl: joinUrl } : {},
|
|
53142
|
+
...unchanged.length ? { warning: `Outlook accepted the update but these did not change: ${unchanged.join(", ")}.` } : {}
|
|
53143
|
+
});
|
|
52607
53144
|
}
|
|
52608
|
-
|
|
52609
|
-
|
|
52610
|
-
|
|
52611
|
-
|
|
52612
|
-
|
|
52613
|
-
|
|
52614
|
-
|
|
52615
|
-
|
|
52616
|
-
|
|
52617
|
-
|
|
52618
|
-
|
|
52619
|
-
|
|
52620
|
-
|
|
52621
|
-
|
|
52622
|
-
|
|
52623
|
-
|
|
52624
|
-
|
|
52625
|
-
|
|
52626
|
-
|
|
52627
|
-
|
|
52628
|
-
|
|
52629
|
-
|
|
52630
|
-
|
|
52631
|
-
|
|
52632
|
-
|
|
52633
|
-
|
|
52634
|
-
|
|
52635
|
-
|
|
52636
|
-
|
|
52637
|
-
|
|
52638
|
-
|
|
52639
|
-
|
|
52640
|
-
|
|
52641
|
-
|
|
52642
|
-
|
|
52643
|
-
|
|
52644
|
-
|
|
52645
|
-
|
|
52646
|
-
|
|
52647
|
-
|
|
52648
|
-
|
|
52649
|
-
|
|
52650
|
-
|
|
52651
|
-
|
|
52652
|
-
|
|
52653
|
-
|
|
52654
|
-
|
|
52655
|
-
|
|
52656
|
-
|
|
52657
|
-
|
|
52658
|
-
|
|
52659
|
-
|
|
52660
|
-
|
|
52661
|
-
|
|
52662
|
-
|
|
52663
|
-
|
|
52664
|
-
|
|
52665
|
-
|
|
52666
|
-
|
|
52667
|
-
|
|
52668
|
-
|
|
52669
|
-
|
|
52670
|
-
|
|
52671
|
-
|
|
52672
|
-
|
|
52673
|
-
|
|
52674
|
-
}
|
|
52675
|
-
|
|
52676
|
-
|
|
52677
|
-
|
|
52678
|
-
|
|
52679
|
-
|
|
52680
|
-
|
|
52681
|
-
|
|
52682
|
-
|
|
52683
|
-
|
|
52684
|
-
|
|
52685
|
-
|
|
52686
|
-
|
|
52687
|
-
|
|
52688
|
-
|
|
52689
|
-
|
|
52690
|
-
|
|
52691
|
-
|
|
52692
|
-
|
|
52693
|
-
|
|
52694
|
-
|
|
52695
|
-
|
|
52696
|
-
|
|
52697
|
-
|
|
52698
|
-
|
|
52699
|
-
|
|
52700
|
-
|
|
52701
|
-
|
|
52702
|
-
|
|
52703
|
-
|
|
52704
|
-
|
|
52705
|
-
|
|
52706
|
-
|
|
52707
|
-
|
|
52708
|
-
|
|
52709
|
-
|
|
52710
|
-
|
|
52711
|
-
|
|
52712
|
-
|
|
52713
|
-
|
|
52714
|
-
|
|
52715
|
-
|
|
52716
|
-
|
|
52717
|
-
|
|
52718
|
-
|
|
52719
|
-
|
|
52720
|
-
|
|
52721
|
-
|
|
52722
|
-
|
|
52723
|
-
|
|
52724
|
-
|
|
52725
|
-
|
|
52726
|
-
|
|
52727
|
-
}
|
|
52728
|
-
return part;
|
|
52729
|
-
}
|
|
52730
|
-
function checkExportId(id) {
|
|
52731
|
-
if (id === void 0)
|
|
52732
|
-
return randomBytes7(12).toString("base64url");
|
|
52733
|
-
if (typeof id !== "string" || !EXPORT_ID.test(id)) {
|
|
52734
|
-
throw badInput2("Export id is malformed.", "Pass the cursor exactly as returned, or start a new export without one.");
|
|
52735
|
-
}
|
|
52736
|
-
return id;
|
|
52737
|
-
}
|
|
52738
|
-
var toBytes = (data) => typeof data === "string" ? new TextEncoder().encode(data) : data;
|
|
52739
|
-
async function openExportSink(opts) {
|
|
52740
|
-
const env = opts.env ?? process.env;
|
|
52741
|
-
const part = checkPart(opts.part ?? 1);
|
|
52742
|
-
const exportId = checkExportId(opts.exportId);
|
|
52743
|
-
const now = opts.now;
|
|
52744
|
-
if (opts.maxBytes !== void 0 && !(Number.isSafeInteger(opts.maxBytes) && opts.maxBytes > 0)) {
|
|
52745
|
-
throw new TypeError(`openExportSink: maxBytes must be a positive whole number, got ${opts.maxBytes}.`);
|
|
52746
|
-
}
|
|
52747
|
-
const store = blobStoreFromEnv(env);
|
|
52748
|
-
if (store)
|
|
52749
|
-
return openBlobSink(store, exportId, part, opts, now);
|
|
52750
|
-
if (opts.existingDir !== void 0) {
|
|
52751
|
-
return openDiskSink(resumeDir(opts.existingDir, opts.envVar, env, exportId, opts.name), exportId, part);
|
|
52752
|
-
}
|
|
52753
|
-
const dir = freshDir(opts, env, now);
|
|
52754
|
-
await writeFileSafe(join10(dir, EXPORT_MARKER), new TextEncoder().encode(JSON.stringify({ exportId, name: opts.name })), {
|
|
52755
|
-
allowedRoots: [dir],
|
|
52756
|
-
mode: 384
|
|
52757
|
-
});
|
|
52758
|
-
return openDiskSink(dir, exportId, part);
|
|
52759
|
-
}
|
|
52760
|
-
function outsideRoots(what, envVar) {
|
|
52761
|
-
return badInput2(`The ${what} is outside the directories exports may be written to.`, `Use a folder under ${envVar} if set, otherwise under ~/Downloads, ~/Documents or ~/Desktop.`);
|
|
52762
|
-
}
|
|
52763
|
-
function within(p, roots, what, envVar) {
|
|
52764
|
-
try {
|
|
52765
|
-
return assertPathWithinRoots(p, roots);
|
|
52766
|
-
} catch {
|
|
52767
|
-
throw outsideRoots(what, envVar);
|
|
52768
|
-
}
|
|
52769
|
-
}
|
|
52770
|
-
function freshDir(opts, env, now) {
|
|
52771
|
-
const roots = exportAllowedRoots(opts.envVar, env);
|
|
52772
|
-
let parent;
|
|
52773
|
-
try {
|
|
52774
|
-
parent = resolveOutputDir(opts.dest, opts.envVar, { env, name: opts.name, allowedRoots: roots });
|
|
52775
|
-
} catch (err) {
|
|
52776
|
-
if (err instanceof McpToolError || err instanceof TypeError)
|
|
52777
|
-
throw err;
|
|
52778
|
-
throw outsideRoots("destination", opts.envVar);
|
|
52779
|
-
}
|
|
52780
|
-
const realParent = assertPathWithinRoots(parent, [parent]);
|
|
52781
|
-
const stem = `${sanitizeExportFileName(sanitizeBaseName(opts.baseName), 80)}-${stamp(now ?? Date.now())}`;
|
|
52782
|
-
for (let n = 1; n <= 1e3; n++) {
|
|
52783
|
-
const candidate = join10(realParent, n === 1 ? stem : `${stem}-${n}`);
|
|
52784
|
-
try {
|
|
52785
|
-
mkdirSync6(candidate, { mode: 448 });
|
|
52786
|
-
return candidate;
|
|
52787
|
-
} catch (err) {
|
|
52788
|
-
if (err.code !== "EEXIST")
|
|
52789
|
-
throw err;
|
|
53145
|
+
);
|
|
53146
|
+
server.registerTool(
|
|
53147
|
+
"outlook_respond_to_invite",
|
|
53148
|
+
{
|
|
53149
|
+
description: "Accept, tentatively accept or decline a meeting invitation. Pass the invite's `messageId` (from outlook_list_unread_messages, which marks it kind \"meetingRequest\") or the event's `eventId` \u2014 exactly one. The organizer is sent your response unless `sendResponse: false`, in which case only your calendar changes and no confirmation is asked. Cancelled meetings and meetings you organize are refused. The result is verified by re-reading the event's response status; a declined meeting may leave your calendar entirely. Outlook then moves the invite message itself to Deleted Items (under a new id), so an answered invite needs no outlook_mark_read. " + CONFIRM_FLOW_SENTENCE + " " + TIMEOUT_SENTENCE + " " + CONFIRM_INJECTION_RULE,
|
|
53150
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
53151
|
+
_meta: workMeta("send", { aliases: ["accept", "decline", "rsvp"] }),
|
|
53152
|
+
inputSchema: external_exports.object({
|
|
53153
|
+
messageId: external_exports.string().min(1).optional().describe("Id of the invite message (resolved to its event). Give this or eventId."),
|
|
53154
|
+
eventId: external_exports.string().min(1).optional().describe("Event Id. Give this or messageId."),
|
|
53155
|
+
response: external_exports.enum(["accept", "tentative", "decline"]).describe("Your answer"),
|
|
53156
|
+
comment: external_exports.string().optional().describe("Note to the organizer sent with the response"),
|
|
53157
|
+
sendResponse: external_exports.boolean().optional().describe("Tell the organizer (default true). false changes only your calendar."),
|
|
53158
|
+
confirmToken: confirmTokenParam
|
|
53159
|
+
})
|
|
53160
|
+
},
|
|
53161
|
+
async ({ messageId, eventId, response, comment, sendResponse, confirmToken }, ctx) => {
|
|
53162
|
+
if (messageId === void 0 === (eventId === void 0)) {
|
|
53163
|
+
throw new McpToolError("Pass exactly one of messageId or eventId.", {
|
|
53164
|
+
hint: "messageId is the invite in your inbox; eventId is the meeting on your calendar."
|
|
53165
|
+
});
|
|
53166
|
+
}
|
|
53167
|
+
let event;
|
|
53168
|
+
if (messageId !== void 0) {
|
|
53169
|
+
const msg = await client2.get(
|
|
53170
|
+
`/me/messages/${encodeURIComponent(messageId)}`,
|
|
53171
|
+
{ query: { $select: `Id,${MEETING_MESSAGE_TYPE}`, $expand: EVENT_EXPAND } }
|
|
53172
|
+
);
|
|
53173
|
+
event = resolvedEvent(msg?.Event);
|
|
53174
|
+
if (!event) {
|
|
53175
|
+
if (isMeetingMessage(msg)) {
|
|
53176
|
+
throw new McpToolError(
|
|
53177
|
+
"This invite's meeting is no longer on your calendar (already declined, cancelled or deleted) \u2014 there is nothing to respond to.",
|
|
53178
|
+
{ hint: "Mark the invite read or file it; answering it again is not possible." }
|
|
53179
|
+
);
|
|
53180
|
+
}
|
|
53181
|
+
throw new McpToolError("That message is not a meeting invite.", {
|
|
53182
|
+
hint: 'Only messages outlook_list_unread_messages marks kind "meetingRequest" carry an event to answer.'
|
|
53183
|
+
});
|
|
53184
|
+
}
|
|
53185
|
+
} else {
|
|
53186
|
+
event = await client2.get(
|
|
53187
|
+
`/me/events/${encodeURIComponent(eventId)}?$select=${INVITE_EVENT_SELECT}`
|
|
53188
|
+
);
|
|
53189
|
+
}
|
|
53190
|
+
const id = event?.Id ?? eventId;
|
|
53191
|
+
if (event?.IsCancelled === true) {
|
|
53192
|
+
throw new McpToolError("This meeting has been cancelled; there is nothing to respond to.", {
|
|
53193
|
+
hint: "Remove it from your calendar with outlook_delete_event if it is still shown."
|
|
53194
|
+
});
|
|
53195
|
+
}
|
|
53196
|
+
if (event?.IsOrganizer === true || event?.ResponseStatus?.Response === "Organizer") {
|
|
53197
|
+
throw new McpToolError("You organize this meeting, so you cannot respond to it.", {
|
|
53198
|
+
hint: "To change or call it off, use outlook_update_event or Outlook itself."
|
|
53199
|
+
});
|
|
53200
|
+
}
|
|
53201
|
+
const { verb, status } = INVITE_RESPONSES[response];
|
|
53202
|
+
const notify = sendResponse !== false;
|
|
53203
|
+
if (!notify && comment) {
|
|
53204
|
+
throw new McpToolError("A comment can only go with a response the organizer receives.", {
|
|
53205
|
+
hint: "Drop the comment, or leave sendResponse at its default (true) so the organizer gets both."
|
|
53206
|
+
});
|
|
53207
|
+
}
|
|
53208
|
+
const path = `/me/events/${encodeURIComponent(id)}/${verb}`;
|
|
53209
|
+
const payload = { ...comment ? { Comment: comment } : {}, SendResponse: notify };
|
|
53210
|
+
if (notify) {
|
|
53211
|
+
const when = `${trimTime2(event?.Start?.DateTime) ?? "?"} to ${trimTime2(event?.End?.DateTime) ?? "?"}${event?.Start?.TimeZone ? ` (${event.Start.TimeZone})` : ""}`;
|
|
53212
|
+
const organizer = event?.Organizer?.EmailAddress?.Address ?? "the organizer";
|
|
53213
|
+
const gate = await confirmWrite(ctx, {
|
|
53214
|
+
tool: "outlook_respond_to_invite",
|
|
53215
|
+
action: "calendar.respond_to_invite",
|
|
53216
|
+
message: "Review and confirm this response (the organizer is notified):",
|
|
53217
|
+
summary: `${response === "tentative" ? "Tentatively accept" : response === "accept" ? "Accept" : "Decline"} "${event?.Subject ?? id}" ${when}, organized by ${organizer}` + (comment ? `, with comment: "${comment}"` : ""),
|
|
53218
|
+
// One signed-in mailbox per server process.
|
|
53219
|
+
account: void 0,
|
|
53220
|
+
target: id,
|
|
53221
|
+
request: { method: "POST", path, body: payload },
|
|
53222
|
+
confirmToken
|
|
53223
|
+
});
|
|
53224
|
+
if (gate) return gate;
|
|
53225
|
+
}
|
|
53226
|
+
const write = await writeOrUnknown2(
|
|
53227
|
+
() => client2.write("POST", path, payload),
|
|
53228
|
+
"the event's response (outlook_get_event)"
|
|
53229
|
+
);
|
|
53230
|
+
if (!write.ok) return write.result;
|
|
53231
|
+
let after;
|
|
53232
|
+
try {
|
|
53233
|
+
after = await client2.get(
|
|
53234
|
+
`/me/events/${encodeURIComponent(id)}?$select=ResponseStatus`
|
|
53235
|
+
);
|
|
53236
|
+
} catch (e) {
|
|
53237
|
+
if (response === "decline" && errorStatusOf(e) === 404) {
|
|
53238
|
+
return minifiedResult({
|
|
53239
|
+
responded: true,
|
|
53240
|
+
eventId: id,
|
|
53241
|
+
response: status,
|
|
53242
|
+
sentResponse: notify,
|
|
53243
|
+
note: "The declined meeting is no longer on your calendar.",
|
|
53244
|
+
inviteMessage: INVITE_FILED_NOTE
|
|
53245
|
+
});
|
|
53246
|
+
}
|
|
53247
|
+
return minifiedResult({
|
|
53248
|
+
responded: null,
|
|
53249
|
+
eventId: id,
|
|
53250
|
+
sentResponse: notify,
|
|
53251
|
+
warning: `Outlook accepted the response but re-reading the event failed (${e instanceof Error ? e.message : String(e)}), so it is unverified.`,
|
|
53252
|
+
inviteMessage: INVITE_FILED_NOTE
|
|
53253
|
+
});
|
|
53254
|
+
}
|
|
53255
|
+
const got = after?.ResponseStatus?.Response;
|
|
53256
|
+
return minifiedResult({
|
|
53257
|
+
responded: got === status,
|
|
53258
|
+
eventId: id,
|
|
53259
|
+
response: got,
|
|
53260
|
+
sentResponse: notify,
|
|
53261
|
+
...got === status ? {} : { warning: `Outlook accepted the response but the status did not change (still ${got ?? "unknown"}).` },
|
|
53262
|
+
inviteMessage: INVITE_FILED_NOTE
|
|
53263
|
+
});
|
|
52790
53264
|
}
|
|
52791
|
-
|
|
52792
|
-
|
|
52793
|
-
|
|
52794
|
-
|
|
52795
|
-
|
|
52796
|
-
}
|
|
52797
|
-
|
|
52798
|
-
|
|
52799
|
-
|
|
52800
|
-
|
|
52801
|
-
|
|
52802
|
-
|
|
52803
|
-
|
|
52804
|
-
|
|
52805
|
-
|
|
52806
|
-
|
|
52807
|
-
|
|
52808
|
-
|
|
52809
|
-
|
|
52810
|
-
|
|
52811
|
-
|
|
52812
|
-
|
|
52813
|
-
|
|
52814
|
-
|
|
52815
|
-
|
|
52816
|
-
|
|
52817
|
-
|
|
52818
|
-
|
|
52819
|
-
|
|
52820
|
-
|
|
52821
|
-
|
|
52822
|
-
|
|
52823
|
-
|
|
52824
|
-
|
|
52825
|
-
|
|
52826
|
-
|
|
52827
|
-
|
|
52828
|
-
|
|
52829
|
-
|
|
52830
|
-
|
|
52831
|
-
|
|
52832
|
-
|
|
52833
|
-
|
|
52834
|
-
|
|
52835
|
-
|
|
52836
|
-
|
|
52837
|
-
|
|
52838
|
-
|
|
52839
|
-
|
|
52840
|
-
|
|
52841
|
-
|
|
52842
|
-
|
|
52843
|
-
|
|
52844
|
-
|
|
52845
|
-
|
|
52846
|
-
|
|
52847
|
-
|
|
52848
|
-
|
|
52849
|
-
|
|
52850
|
-
|
|
52851
|
-
|
|
52852
|
-
}
|
|
52853
|
-
|
|
52854
|
-
|
|
52855
|
-
|
|
52856
|
-
|
|
52857
|
-
mode: "disk",
|
|
52858
|
-
exportId,
|
|
52859
|
-
part,
|
|
52860
|
-
dir,
|
|
52861
|
-
async addFile(relPath, data, _contentType, o = {}) {
|
|
52862
|
-
if (finished)
|
|
52863
|
-
throw badInput2("This export part is already finished.");
|
|
52864
|
-
const segs = checkRelPath(relPath);
|
|
52865
|
-
if (files.has(relPath) && !o.overwrite)
|
|
52866
|
-
throw badInput2(`Export file ${relPath} was already written.`);
|
|
52867
|
-
const target = join10(dir, ...segs);
|
|
52868
|
-
const parent = within(dirname5(target), [dir], "export file", "the export");
|
|
52869
|
-
await mkdir4(parent, { recursive: true, mode: 448 });
|
|
52870
|
-
const bytes = toBytes(data);
|
|
52871
|
-
const path = await writeFileSafe(target, bytes, {
|
|
52872
|
-
allowedRoots: [dir],
|
|
52873
|
-
mode: 384,
|
|
52874
|
-
...o.overwrite ? { overwrite: true } : {}
|
|
53265
|
+
);
|
|
53266
|
+
server.registerTool(
|
|
53267
|
+
"outlook_delete_event",
|
|
53268
|
+
{
|
|
53269
|
+
description: `Delete a calendar event. An occurrence's id deletes only that occurrence; a series master's id (Type "SeriesMaster" in outlook_get_event) deletes the WHOLE series. Only the organizer can delete a meeting \u2014 for an invite you received, decline it with outlook_respond_to_invite instead (a meeting the organizer already cancelled can be removed here). Deleting a meeting that has attendees sends each of them a cancellation, so it is confirmed first; your own appointment with no attendees, or a cancelled meeting, is removed without asking because only your calendar changes. The result is verified by re-reading the event. ` + CONFIRM_FLOW_SENTENCE + " " + TIMEOUT_SENTENCE + " " + CONFIRM_INJECTION_RULE,
|
|
53270
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
|
|
53271
|
+
_meta: workMeta("change", { aliases: ["cancel the meeting"] }),
|
|
53272
|
+
inputSchema: external_exports.object({
|
|
53273
|
+
id: external_exports.string().min(1).describe("Event Id from outlook_list_events or outlook_get_event (an occurrence, or a series master)"),
|
|
53274
|
+
confirmToken: confirmTokenParam
|
|
53275
|
+
})
|
|
53276
|
+
},
|
|
53277
|
+
async ({ id, confirmToken }, ctx) => {
|
|
53278
|
+
const path = `/me/events/${encodeURIComponent(id)}`;
|
|
53279
|
+
const zone = await mailboxTimeZone(client2);
|
|
53280
|
+
const event = await client2.get(
|
|
53281
|
+
`${path}?$select=${DELETE_EVENT_SELECT}`,
|
|
53282
|
+
zone ? { prefer: `outlook.timezone="${zone}"` } : {}
|
|
53283
|
+
);
|
|
53284
|
+
const organizer = event?.IsOrganizer === true || event?.ResponseStatus?.Response === "Organizer";
|
|
53285
|
+
const cancelled = event?.IsCancelled === true;
|
|
53286
|
+
if (!organizer && !cancelled) {
|
|
53287
|
+
throw new McpToolError("Only the organizer can delete this meeting.", {
|
|
53288
|
+
hint: 'You are an attendee: deleting your copy would not tell the organizer. To drop it, decline with outlook_respond_to_invite (response: "decline"), which also removes it from your calendar.'
|
|
53289
|
+
});
|
|
53290
|
+
}
|
|
53291
|
+
const scope = event?.Type === "SeriesMaster" ? "the whole series (every occurrence)" : event?.Type === "Occurrence" || event?.Type === "Exception" ? "only this occurrence (the rest of the series stays)" : "this event";
|
|
53292
|
+
const attendeeCount = event?.Attendees?.length ?? 0;
|
|
53293
|
+
const notifies = organizer && !cancelled && attendeeCount > 0;
|
|
53294
|
+
if (notifies) {
|
|
53295
|
+
const when = `${trimTime2(event?.Start?.DateTime) ?? "?"} to ${trimTime2(event?.End?.DateTime) ?? "?"}${event?.Start?.TimeZone ? ` (${event.Start.TimeZone})` : ""}`;
|
|
53296
|
+
const gate = await confirmWrite(ctx, {
|
|
53297
|
+
tool: "outlook_delete_event",
|
|
53298
|
+
action: "calendar.delete_event",
|
|
53299
|
+
message: "Review and confirm this deletion (every attendee is sent a cancellation):",
|
|
53300
|
+
summary: `Delete "${event?.Subject ?? id}" ${when} \u2014 ${scope}; ${attendeeCount} attendee${attendeeCount === 1 ? "" : "s"} will be sent a cancellation`,
|
|
53301
|
+
// One signed-in mailbox per server process.
|
|
53302
|
+
account: void 0,
|
|
53303
|
+
target: id,
|
|
53304
|
+
request: { method: "DELETE", path },
|
|
53305
|
+
preview: { scope, attendees: attendeeCount },
|
|
53306
|
+
confirmToken
|
|
53307
|
+
});
|
|
53308
|
+
if (gate) return gate;
|
|
53309
|
+
}
|
|
53310
|
+
const write = await writeOrUnknown2(
|
|
53311
|
+
() => client2.write("DELETE", path),
|
|
53312
|
+
"the calendar (outlook_get_event)"
|
|
53313
|
+
);
|
|
53314
|
+
if (!write.ok) return write.result;
|
|
53315
|
+
const base = { Id: id, Subject: event?.Subject, scope, cancellationSent: notifies };
|
|
53316
|
+
let after;
|
|
53317
|
+
try {
|
|
53318
|
+
after = await client2.get(`${path}?$select=Id`);
|
|
53319
|
+
} catch (e) {
|
|
53320
|
+
if (errorStatusOf(e) === 404) return minifiedResult({ deleted: true, ...base });
|
|
53321
|
+
return minifiedResult({
|
|
53322
|
+
deleted: null,
|
|
53323
|
+
...base,
|
|
53324
|
+
warning: `Outlook accepted the delete but re-reading the event failed (${e instanceof Error ? e.message : String(e)}), so it is unverified.`
|
|
53325
|
+
});
|
|
53326
|
+
}
|
|
53327
|
+
return minifiedResult({
|
|
53328
|
+
deleted: false,
|
|
53329
|
+
...base,
|
|
53330
|
+
warning: `Outlook accepted the delete but the event is still on the calendar${after?.Id ? ` (${after.Id})` : ""}.`
|
|
52875
53331
|
});
|
|
52876
|
-
const rec = { name: relPath, path, bytes: bytes.byteLength };
|
|
52877
|
-
files.delete(relPath);
|
|
52878
|
-
files.set(relPath, rec);
|
|
52879
|
-
return rec;
|
|
52880
|
-
},
|
|
52881
|
-
async finish() {
|
|
52882
|
-
if (finished)
|
|
52883
|
-
throw badInput2("This export part is already finished.");
|
|
52884
|
-
finished = true;
|
|
52885
|
-
const list = [...files.values()];
|
|
52886
|
-
return {
|
|
52887
|
-
mode: "disk",
|
|
52888
|
-
exportId,
|
|
52889
|
-
part,
|
|
52890
|
-
location: dir,
|
|
52891
|
-
dir,
|
|
52892
|
-
bytes: list.reduce((n, f) => n + f.bytes, 0),
|
|
52893
|
-
files: list
|
|
52894
|
-
};
|
|
52895
53332
|
}
|
|
52896
|
-
|
|
52897
|
-
|
|
52898
|
-
|
|
52899
|
-
|
|
52900
|
-
|
|
52901
|
-
|
|
52902
|
-
|
|
52903
|
-
|
|
52904
|
-
|
|
52905
|
-
|
|
52906
|
-
|
|
52907
|
-
|
|
52908
|
-
|
|
52909
|
-
|
|
52910
|
-
|
|
52911
|
-
|
|
52912
|
-
|
|
52913
|
-
|
|
52914
|
-
|
|
52915
|
-
|
|
52916
|
-
const bytes = toBytes(data);
|
|
52917
|
-
const replaced = entries.get(relPath)?.bytes.byteLength ?? 0;
|
|
52918
|
-
if (bytes.byteLength > maxBytes) {
|
|
52919
|
-
throw new McpToolError(`Export file ${relPath} alone is ${bytes.byteLength} bytes, over the ${maxBytes}-byte part limit.`, {
|
|
52920
|
-
kind: "too_large",
|
|
52921
|
-
hint: "No export part can hold this file: skip it, or raise the part limit (maxBytes)."
|
|
53333
|
+
);
|
|
53334
|
+
server.registerTool(
|
|
53335
|
+
"outlook_reply",
|
|
53336
|
+
{
|
|
53337
|
+
description: "Reply, reply-all or forward a message, with `comment` as your new text above the quoted original. Outlook picks the recipients for reply (the sender, or its Reply-To) and reply-all (sender plus the original To and Cc, minus you); a forward needs `to`. `draftOnly: true` leaves the reply in Drafts and returns its id, sending nothing and asking no confirmation; otherwise it is sent at once. " + CONFIRM_FLOW_SENTENCE + " The preview shows the recipients, subject and comment. " + TIMEOUT_SENTENCE + " " + CONFIRM_INJECTION_RULE + " " + UNTRUSTED_DESCRIPTION_SUFFIX,
|
|
53338
|
+
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
|
|
53339
|
+
_meta: workMeta("send", { aliases: ["reply", "reply all", "forward"] }),
|
|
53340
|
+
inputSchema: external_exports.object({
|
|
53341
|
+
messageId: external_exports.string().min(1).describe("Id of the message to answer or forward"),
|
|
53342
|
+
mode: external_exports.enum(["reply", "replyAll", "forward"]).describe("reply (sender only), replyAll, or forward"),
|
|
53343
|
+
comment: external_exports.string().describe("Your text, placed above the quoted original"),
|
|
53344
|
+
to: recipientList.describe("Forward recipients' email addresses (forward only, required there)"),
|
|
53345
|
+
draftOnly: external_exports.boolean().optional().describe("Save to Drafts instead of sending (default false)"),
|
|
53346
|
+
confirmToken: confirmTokenParam
|
|
53347
|
+
})
|
|
53348
|
+
},
|
|
53349
|
+
async ({ messageId, mode, comment, to, draftOnly, confirmToken }, ctx) => {
|
|
53350
|
+
if (mode === "forward" && !to?.length) {
|
|
53351
|
+
throw new McpToolError("A forward needs at least one recipient.", {
|
|
53352
|
+
hint: "Pass `to` with the addresses to forward to."
|
|
52922
53353
|
});
|
|
52923
53354
|
}
|
|
52924
|
-
if (
|
|
52925
|
-
throw new McpToolError(`
|
|
52926
|
-
|
|
52927
|
-
hint: "Finish this part and continue the export in the next one (return the resume cursor)."
|
|
53355
|
+
if (mode !== "forward" && to?.length) {
|
|
53356
|
+
throw new McpToolError("`to` applies only to a forward.", {
|
|
53357
|
+
hint: 'A reply goes where Outlook computes; use mode "forward" to send it somewhere else, or outlook_send_message.'
|
|
52928
53358
|
});
|
|
52929
53359
|
}
|
|
52930
|
-
|
|
52931
|
-
|
|
52932
|
-
|
|
52933
|
-
|
|
52934
|
-
|
|
52935
|
-
|
|
52936
|
-
|
|
52937
|
-
|
|
52938
|
-
|
|
52939
|
-
|
|
52940
|
-
|
|
52941
|
-
|
|
52942
|
-
|
|
52943
|
-
|
|
52944
|
-
|
|
52945
|
-
|
|
53360
|
+
const { send, draft, prefix } = REPLY_MODES[mode];
|
|
53361
|
+
const base = `/me/messages/${encodeURIComponent(messageId)}`;
|
|
53362
|
+
const forwardTo = mode === "forward" ? { ToRecipients: toRecipients(to) } : {};
|
|
53363
|
+
if (draftOnly === true) {
|
|
53364
|
+
const create = await writeOrUnknown2(
|
|
53365
|
+
() => client2.write(
|
|
53366
|
+
"POST",
|
|
53367
|
+
`${base}/${draft}`,
|
|
53368
|
+
{}
|
|
53369
|
+
),
|
|
53370
|
+
"Drafts"
|
|
53371
|
+
);
|
|
53372
|
+
if (!create.ok) return create.result;
|
|
53373
|
+
const created = create.value;
|
|
53374
|
+
if (!created?.Id) {
|
|
53375
|
+
throw new McpToolError("Outlook did not return the draft it created.", {
|
|
53376
|
+
hint: "Check the Drafts folder before trying again."
|
|
53377
|
+
});
|
|
53378
|
+
}
|
|
53379
|
+
try {
|
|
53380
|
+
await client2.write("PATCH", `/me/messages/${encodeURIComponent(created.Id)}`, {
|
|
53381
|
+
Body: bodyWithComment(created.Body, comment),
|
|
53382
|
+
...forwardTo
|
|
53383
|
+
});
|
|
53384
|
+
} catch (e) {
|
|
53385
|
+
const what = `your comment${mode === "forward" ? " and recipients" : ""}`;
|
|
53386
|
+
const reason = e instanceof Error ? e.message : String(e);
|
|
53387
|
+
if (isCredentialFailure(e)) {
|
|
53388
|
+
throw new McpToolError(
|
|
53389
|
+
`The reply draft was created (draftId ${created.Id}) but adding ${what} failed: ${reason}`,
|
|
53390
|
+
{
|
|
53391
|
+
hint: `${e instanceof McpToolError && e.hint ? `${e.hint} ` : ""}Then edit draft ${created.Id} in Drafts, or delete it before trying again.`,
|
|
53392
|
+
cause: e,
|
|
53393
|
+
kind: errorKindOf(e),
|
|
53394
|
+
status: errorStatusOf(e)
|
|
53395
|
+
}
|
|
53396
|
+
);
|
|
53397
|
+
}
|
|
53398
|
+
const warning = e instanceof WriteOutcomeUnknownError ? `The draft was created, and ${what} may have been applied: Outlook did not confirm the edit (${e.timedOut ? "it did not answer in time" : "the connection dropped"}). Check the draft in Drafts before editing it again or trying again.` : `The draft was created but adding ${what} failed (${reason}). Edit it in Drafts, or delete it before trying again.`;
|
|
53399
|
+
return minifiedResult({
|
|
53400
|
+
drafted: true,
|
|
53401
|
+
mode,
|
|
53402
|
+
draftId: created.Id,
|
|
53403
|
+
WebLink: created.WebLink,
|
|
53404
|
+
warning
|
|
53405
|
+
});
|
|
53406
|
+
}
|
|
53407
|
+
return minifiedResult({ drafted: true, mode, draftId: created.Id, WebLink: created.WebLink });
|
|
53408
|
+
}
|
|
53409
|
+
const orig = await client2.get(base, {
|
|
53410
|
+
query: { $select: "Subject,From,ReplyTo,ToRecipients,CcRecipients" }
|
|
52946
53411
|
});
|
|
52947
|
-
const
|
|
52948
|
-
const
|
|
53412
|
+
const sender = orig?.ReplyTo?.length ? orig.ReplyTo : orig?.From ? [orig.From] : [];
|
|
53413
|
+
const recipients = mode === "forward" ? { to: to ?? [], cc: [] } : mode === "replyAll" ? { to: addresses(sender, orig?.ToRecipients), cc: addresses(orig?.CcRecipients) } : { to: addresses(sender), cc: [] };
|
|
53414
|
+
const subject = `${prefix}${orig?.Subject ?? ""}`;
|
|
53415
|
+
const path = `${base}/${send}`;
|
|
53416
|
+
const payload = { Comment: comment, ...forwardTo };
|
|
53417
|
+
const everyone = [...recipients.to, ...recipients.cc.map((c) => `cc ${c}`)];
|
|
53418
|
+
const who = everyone.length > MAX_RECIPIENTS ? `${everyone.slice(0, MAX_RECIPIENTS).join(", ")} and ${(everyone.length - MAX_RECIPIENTS).toLocaleString("en-US")} more` : everyone.join(", ");
|
|
53419
|
+
const gate = await confirmWrite(ctx, {
|
|
53420
|
+
tool: "outlook_reply",
|
|
53421
|
+
action: `mail.${send}`,
|
|
53422
|
+
message: "Review and confirm this email before it is sent:",
|
|
53423
|
+
summary: `${mode === "forward" ? "Forward" : mode === "replyAll" ? "Reply all" : "Reply"} "${subject}" to ${who || "(no recipients)"}${mode === "replyAll" ? " (Outlook leaves out your own address)" : ""}, with comment: "${comment}"`,
|
|
53424
|
+
// One signed-in mailbox per server process.
|
|
53425
|
+
account: void 0,
|
|
53426
|
+
target: messageId,
|
|
53427
|
+
request: { method: "POST", path, body: payload },
|
|
53428
|
+
confirmToken
|
|
53429
|
+
});
|
|
53430
|
+
if (gate) return gate;
|
|
53431
|
+
const sent = await writeOrUnknown2(() => client2.write("POST", path, payload), "Sent Items");
|
|
53432
|
+
if (!sent.ok) return sent.result;
|
|
53433
|
+
return mailboxUntrusted({
|
|
53434
|
+
sent: true,
|
|
53435
|
+
mode,
|
|
53436
|
+
subject,
|
|
53437
|
+
...cappedRecipients("to", recipients.to),
|
|
53438
|
+
...cappedRecipients("cc", recipients.cc)
|
|
53439
|
+
});
|
|
53440
|
+
}
|
|
53441
|
+
);
|
|
53442
|
+
}
|
|
53443
|
+
|
|
53444
|
+
// src/tools/healthcheck.ts
|
|
53445
|
+
init_healthcheck();
|
|
53446
|
+
function registerHealthcheckTool(server, client2) {
|
|
53447
|
+
registerCredentialHealthcheckTool({
|
|
53448
|
+
server,
|
|
53449
|
+
prefix: "outlook",
|
|
53450
|
+
hostLabel: "outlook.office.com",
|
|
53451
|
+
probePath: "/api/v2.0/me",
|
|
53452
|
+
/**
|
|
53453
|
+
* Resolve the credential the way the real tools do — same TokenManager,
|
|
53454
|
+
* same cache, same capture — so a passing healthcheck means real tools
|
|
53455
|
+
* work. Returns a SOURCE LABEL and non-secret detail, never the token:
|
|
53456
|
+
* this result is what people paste into a chat when something is broken.
|
|
53457
|
+
*/
|
|
53458
|
+
resolveCredential: async () => {
|
|
53459
|
+
await client2.refreshTokenIfNeeded();
|
|
53460
|
+
const expiresAt = client2.tokenExpiresAt();
|
|
52949
53461
|
return {
|
|
52950
|
-
|
|
52951
|
-
|
|
52952
|
-
|
|
52953
|
-
|
|
52954
|
-
|
|
52955
|
-
|
|
52956
|
-
archiveBytes: zip.byteLength,
|
|
52957
|
-
bytes: files.reduce((n, f) => n + f.bytes, 0),
|
|
52958
|
-
files
|
|
53462
|
+
source: client2.tokenSource,
|
|
53463
|
+
detail: {
|
|
53464
|
+
expiresAt: expiresAt === null ? null : new Date(expiresAt).toISOString(),
|
|
53465
|
+
minutesRemaining: expiresAt === null ? null : Math.round((expiresAt - Date.now()) / 6e4),
|
|
53466
|
+
apiBase: client2.apiBase
|
|
53467
|
+
}
|
|
52959
53468
|
};
|
|
53469
|
+
},
|
|
53470
|
+
probeFn: async () => {
|
|
53471
|
+
const me = await client2.get("/me");
|
|
53472
|
+
return `authenticated as ${me?.EmailAddress ?? "unknown"}`;
|
|
52960
53473
|
}
|
|
52961
|
-
};
|
|
52962
|
-
}
|
|
52963
|
-
var EXPORT_CURSOR_MAX_LENGTH = 16384;
|
|
52964
|
-
var CURSOR_VERSION = 1;
|
|
52965
|
-
function encodeExportCursor(state) {
|
|
52966
|
-
const out = Buffer.from(JSON.stringify({ v: CURSOR_VERSION, ...state }), "utf8").toString("base64url");
|
|
52967
|
-
if (out.length > EXPORT_CURSOR_MAX_LENGTH) {
|
|
52968
|
-
throw new TypeError(`encodeExportCursor: cursor is ${out.length} characters (max ${EXPORT_CURSOR_MAX_LENGTH}).`);
|
|
52969
|
-
}
|
|
52970
|
-
return out;
|
|
52971
|
-
}
|
|
52972
|
-
function decodeExportCursor(cursor) {
|
|
52973
|
-
const bad = () => badInput2("The export cursor is not valid.", "Pass the cursor exactly as the previous call returned it, or start a new export without one.");
|
|
52974
|
-
if (typeof cursor !== "string" || cursor.length === 0 || cursor.length > EXPORT_CURSOR_MAX_LENGTH)
|
|
52975
|
-
throw bad();
|
|
52976
|
-
if (!/^[A-Za-z0-9_-]+$/.test(cursor))
|
|
52977
|
-
throw bad();
|
|
52978
|
-
let parsed;
|
|
52979
|
-
try {
|
|
52980
|
-
parsed = JSON.parse(Buffer.from(cursor, "base64url").toString("utf8"));
|
|
52981
|
-
} catch {
|
|
52982
|
-
throw bad();
|
|
52983
|
-
}
|
|
52984
|
-
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed))
|
|
52985
|
-
throw bad();
|
|
52986
|
-
const obj = parsed;
|
|
52987
|
-
if (obj.v !== CURSOR_VERSION)
|
|
52988
|
-
throw bad();
|
|
52989
|
-
return obj;
|
|
52990
|
-
}
|
|
52991
|
-
function jsonlLine(value) {
|
|
52992
|
-
const s = JSON.stringify(value);
|
|
52993
|
-
if (s === void 0)
|
|
52994
|
-
throw new TypeError("jsonlLine: value has no JSON representation.");
|
|
52995
|
-
return `${s}
|
|
52996
|
-
`;
|
|
53474
|
+
});
|
|
52997
53475
|
}
|
|
52998
53476
|
|
|
52999
53477
|
// src/tools/export.ts
|
|
53000
|
-
|
|
53478
|
+
init_zod();
|
|
53479
|
+
init_dist2();
|
|
53480
|
+
import { createHash as createHash5 } from "node:crypto";
|
|
53481
|
+
import { readdirSync as readdirSync2, readFileSync as readFileSync4, rmdirSync, unlinkSync as unlinkSync5 } from "node:fs";
|
|
53482
|
+
import { join as join12 } from "node:path";
|
|
53483
|
+
var SERVER_NAME2 = "office-outlook-mcp";
|
|
53001
53484
|
var ENV_VAR = "OUTLOOK_OUTPUT_DIR";
|
|
53002
53485
|
var MAX_TOP2 = 1e3;
|
|
53003
53486
|
var DEFAULT_MAX_ITEMS = 5e3;
|
|
@@ -53082,6 +53565,7 @@ var eventArgsSchema = external_exports.object({
|
|
|
53082
53565
|
attachments: external_exports.boolean().optional()
|
|
53083
53566
|
});
|
|
53084
53567
|
var positionSchema = external_exports.union([
|
|
53568
|
+
external_exports.object({ at: external_exports.object({ t: external_exports.string().min(1), ids: external_exports.array(external_exports.string()) }) }),
|
|
53085
53569
|
external_exports.object({ from: external_exports.string().min(1) }),
|
|
53086
53570
|
external_exports.object({ link: external_exports.string().min(1).optional(), offset: external_exports.number().int().min(0).max(MAX_TOP2) })
|
|
53087
53571
|
]);
|
|
@@ -53292,16 +53776,16 @@ async function linkBatch(pos, limit, first, follow, keep = () => true) {
|
|
|
53292
53776
|
return end;
|
|
53293
53777
|
}
|
|
53294
53778
|
function shortId(id) {
|
|
53295
|
-
return
|
|
53779
|
+
return createHash5("sha256").update(id).digest("hex").slice(0, 16);
|
|
53296
53780
|
}
|
|
53297
53781
|
function uniqueName(name, used) {
|
|
53298
|
-
let
|
|
53782
|
+
let candidate2 = name;
|
|
53299
53783
|
const dot = name.lastIndexOf(".");
|
|
53300
53784
|
const stem = dot > 0 ? name.slice(0, dot) : name;
|
|
53301
53785
|
const ext = dot > 0 ? name.slice(dot) : "";
|
|
53302
|
-
for (let n = 2; used.has(
|
|
53303
|
-
used.add(
|
|
53304
|
-
return
|
|
53786
|
+
for (let n = 2; used.has(candidate2.toLowerCase()); n++) candidate2 = `${stem}-${n}${ext}`;
|
|
53787
|
+
used.add(candidate2.toLowerCase());
|
|
53788
|
+
return candidate2;
|
|
53305
53789
|
}
|
|
53306
53790
|
var ExportStopped = class extends Error {
|
|
53307
53791
|
};
|
|
@@ -53426,7 +53910,7 @@ async function runExport(client2, opts, input2, spec) {
|
|
|
53426
53910
|
const part = resumed?.part ?? 1;
|
|
53427
53911
|
let pos = spec.pos;
|
|
53428
53912
|
const sink = await openExportSink({
|
|
53429
|
-
name:
|
|
53913
|
+
name: SERVER_NAME2,
|
|
53430
53914
|
envVar: ENV_VAR,
|
|
53431
53915
|
baseName: spec.baseName,
|
|
53432
53916
|
...input2.dest !== void 0 && !resumed ? { dest: input2.dest } : {},
|
|
@@ -53492,7 +53976,7 @@ async function runExport(client2, opts, input2, spec) {
|
|
|
53492
53976
|
try {
|
|
53493
53977
|
const entries = readdirSync2(sink.dir);
|
|
53494
53978
|
if (entries.every((f) => f === EXPORT_MARKER)) {
|
|
53495
|
-
if (entries.length) unlinkSync5(
|
|
53979
|
+
if (entries.length) unlinkSync5(join12(sink.dir, EXPORT_MARKER));
|
|
53496
53980
|
rmdirSync(sink.dir);
|
|
53497
53981
|
}
|
|
53498
53982
|
} catch {
|
|
@@ -53559,7 +54043,7 @@ async function finishPart(client2, sink, run) {
|
|
|
53559
54043
|
const { spec } = run;
|
|
53560
54044
|
const account = run.lookupAccount ? await accountOf(client2) : void 0;
|
|
53561
54045
|
const header = {
|
|
53562
|
-
source:
|
|
54046
|
+
source: SERVER_NAME2,
|
|
53563
54047
|
kind: spec.kind,
|
|
53564
54048
|
...account ? { account } : {},
|
|
53565
54049
|
generated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -53573,7 +54057,7 @@ async function finishPart(client2, sink, run) {
|
|
|
53573
54057
|
|
|
53574
54058
|
> ${EXPORT_NOTE}
|
|
53575
54059
|
|
|
53576
|
-
- Source: ${
|
|
54060
|
+
- Source: ${SERVER_NAME2}${account ? ` (${account})` : ""}
|
|
53577
54061
|
- Generated: ${header.generated_at}
|
|
53578
54062
|
- Export: ${sink.exportId}
|
|
53579
54063
|
|
|
@@ -53582,7 +54066,7 @@ async function finishPart(client2, sink, run) {
|
|
|
53582
54066
|
` : jsonlLine({ _export: header });
|
|
53583
54067
|
const partRec = await sink.addFile(run.name, head + run.chunks.join(""), markdown ? "text/markdown" : "application/jsonl");
|
|
53584
54068
|
const manifest = {
|
|
53585
|
-
source:
|
|
54069
|
+
source: SERVER_NAME2,
|
|
53586
54070
|
kind: spec.kind,
|
|
53587
54071
|
export_id: sink.exportId,
|
|
53588
54072
|
part: sink.part,
|
|
@@ -53609,7 +54093,7 @@ function earlierManifestFiles(sink, current) {
|
|
|
53609
54093
|
if (sink.mode !== "disk" || !sink.dir || sink.part <= 1) return [];
|
|
53610
54094
|
let parsed;
|
|
53611
54095
|
try {
|
|
53612
|
-
parsed = JSON.parse(readFileSync4(
|
|
54096
|
+
parsed = JSON.parse(readFileSync4(join12(sink.dir, "manifest.json"), "utf8"));
|
|
53613
54097
|
} catch {
|
|
53614
54098
|
return [];
|
|
53615
54099
|
}
|
|
@@ -53683,11 +54167,11 @@ function registerExportTools(server, client2, opts = {}) {
|
|
|
53683
54167
|
const folder = args.search !== void 0 ? "all" : args.folder ?? "inbox";
|
|
53684
54168
|
const path = folder === "all" ? "/me/messages" : `/me/mailfolders/${encodeURIComponent(folder)}/messages`;
|
|
53685
54169
|
const perPage = args.attachments === true ? Math.min(pageSize, ATTACHMENT_PAGE_SIZE) : pageSize;
|
|
53686
|
-
const
|
|
53687
|
-
|
|
54170
|
+
const stored = resumed?.pos;
|
|
54171
|
+
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
54172
|
const fetch2 = async (p, remaining) => {
|
|
53689
|
-
if ("
|
|
53690
|
-
const at =
|
|
54173
|
+
if ("at" in p) {
|
|
54174
|
+
const { at } = p;
|
|
53691
54175
|
const limit = Math.min(remaining, perPage);
|
|
53692
54176
|
const { bound, top } = fromQuery(at, limit);
|
|
53693
54177
|
const filter = until ? `${bound} and ReceivedDateTime lt ${until}` : bound;
|
|
@@ -53704,7 +54188,7 @@ function registerExportTools(server, client2, opts = {}) {
|
|
|
53704
54188
|
posAfter: () => p
|
|
53705
54189
|
};
|
|
53706
54190
|
}
|
|
53707
|
-
if (page.has_more && page.next_from ===
|
|
54191
|
+
if (page.has_more && page.next_from === encodeFrom(at)) {
|
|
53708
54192
|
return {
|
|
53709
54193
|
rows: page.rows,
|
|
53710
54194
|
pos: p,
|
|
@@ -53715,10 +54199,10 @@ function registerExportTools(server, client2, opts = {}) {
|
|
|
53715
54199
|
}
|
|
53716
54200
|
return {
|
|
53717
54201
|
rows: page.rows,
|
|
53718
|
-
pos: {
|
|
54202
|
+
pos: { at: decodeFrom(page.next_from) },
|
|
53719
54203
|
hasMore: page.has_more,
|
|
53720
54204
|
// 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 ? {
|
|
54205
|
+
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
54206
|
};
|
|
53723
54207
|
}
|
|
53724
54208
|
return linkBatch(
|
|
@@ -53806,8 +54290,9 @@ function registerExportTools(server, client2, opts = {}) {
|
|
|
53806
54290
|
}
|
|
53807
54291
|
const prefer = args.time_zone ? `outlook.timezone="${args.time_zone}"` : void 0;
|
|
53808
54292
|
const path = args.calendar !== void 0 ? `/me/calendars/${encodeURIComponent(args.calendar)}/calendarview` : "/me/calendarview";
|
|
53809
|
-
const
|
|
53810
|
-
if ("
|
|
54293
|
+
const stored = resumed?.pos ?? { offset: 0 };
|
|
54294
|
+
if (!("offset" in stored)) throw new McpToolError("`cursor` is not a valid export cursor for this tool.");
|
|
54295
|
+
const pos = stored;
|
|
53811
54296
|
const fetch2 = (p, remaining) => linkBatch(
|
|
53812
54297
|
p,
|
|
53813
54298
|
remaining,
|