@chrischall/office-outlook-mcp 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +3 -3
- package/.claude-plugin/plugin.json +3 -3
- package/README.md +74 -19
- package/dist/bundle.js +1384 -211
- package/dist/client.js +29 -9
- package/dist/timezone.js +32 -3
- package/dist/tools/_invite.js +24 -0
- package/dist/tools/_paging.js +9 -2
- package/dist/tools/calendar.js +58 -37
- package/dist/tools/directory.js +36 -31
- package/dist/tools/healthcheck.js +4 -3
- package/dist/tools/mail.js +310 -25
- package/dist/tools/writes.js +579 -28
- package/dist/version.js +1 -1
- package/dist/view.js +44 -7
- package/mint.yaml +3 -2
- package/package.json +2 -2
- package/server.json +15 -3
- package/skills/outlook-fpx/references/outlook-env.sh +2 -2
package/dist/bundle.js
CHANGED
|
@@ -35334,6 +35334,30 @@ var init_dist = __esm({
|
|
|
35334
35334
|
});
|
|
35335
35335
|
|
|
35336
35336
|
// node_modules/@chrischall/mcp-utils/dist/errors/index.js
|
|
35337
|
+
function firstDeclared(err, read) {
|
|
35338
|
+
let cur = err;
|
|
35339
|
+
for (let depth = 0; depth < DECLARATION_CAUSE_DEPTH && cur !== null && typeof cur === "object"; depth++) {
|
|
35340
|
+
const found = read(cur);
|
|
35341
|
+
if (found !== void 0)
|
|
35342
|
+
return found;
|
|
35343
|
+
cur = cur.cause;
|
|
35344
|
+
}
|
|
35345
|
+
return void 0;
|
|
35346
|
+
}
|
|
35347
|
+
function errorStatusOf(err) {
|
|
35348
|
+
return firstDeclared(err, (link2) => {
|
|
35349
|
+
const e = link2;
|
|
35350
|
+
if (typeof e.status === "number")
|
|
35351
|
+
return e.status;
|
|
35352
|
+
return typeof e.statusCode === "number" ? e.statusCode : void 0;
|
|
35353
|
+
});
|
|
35354
|
+
}
|
|
35355
|
+
function errorKindOf(err) {
|
|
35356
|
+
return firstDeclared(err, (link2) => {
|
|
35357
|
+
const k = link2.kind;
|
|
35358
|
+
return typeof k === "string" && MCP_TOOL_ERROR_KINDS.has(k) ? k : void 0;
|
|
35359
|
+
});
|
|
35360
|
+
}
|
|
35337
35361
|
function redactJsonCookie(quote) {
|
|
35338
35362
|
const stringRe = quote === '"' ? DQ_STRING_RE : SQ_STRING_RE;
|
|
35339
35363
|
return (_m, prefix, key, value) => {
|
|
@@ -35361,21 +35385,82 @@ function messageOf(err) {
|
|
|
35361
35385
|
return err.message;
|
|
35362
35386
|
return String(err);
|
|
35363
35387
|
}
|
|
35364
|
-
var DEFAULT_ERROR_MESSAGE_MAX, McpToolError, HEADER_NAME_RUN, BEARER_RE, JWT_RE, BASIC_AUTH_RE, SET_COOKIE_RE, COOKIE_HEADER_RE, API_KEY_RE, GOOGLE_OAUTH_TOKEN_RE, QUERY_SECRET_RE, OAUTH_CODE_RE, FORM_PASSWORD_RE, HEADER_SECRET_RE, AWS_SIGV4_RE, JSON_SECRET_KEYS, JSON_SECRET_DQ_RE, JSON_SECRET_SQ_RE, JSON_SECRET_NUM_RE, JSON_COOKIE_DQ_RE, JSON_COOKIE_SQ_RE, DQ_STRING_RE, SQ_STRING_RE, ERROR_REDACTION_INPUT_MAX;
|
|
35388
|
+
var DEFAULT_ERROR_MESSAGE_MAX, MCP_TOOL_ERROR_KINDS, McpToolError, DECLARATION_CAUSE_DEPTH, UPSTREAM_BODY_PHRASE, UpstreamFormatError, HEADER_NAME_RUN, BEARER_RE, JWT_RE, BASIC_AUTH_RE, SET_COOKIE_RE, COOKIE_HEADER_RE, API_KEY_RE, GOOGLE_OAUTH_TOKEN_RE, QUERY_SECRET_RE, OAUTH_CODE_RE, FORM_PASSWORD_RE, HEADER_SECRET_RE, AWS_SIGV4_RE, JSON_SECRET_KEYS, JSON_SECRET_DQ_RE, JSON_SECRET_SQ_RE, JSON_SECRET_NUM_RE, JSON_COOKIE_DQ_RE, JSON_COOKIE_SQ_RE, DQ_STRING_RE, SQ_STRING_RE, ERROR_REDACTION_INPUT_MAX;
|
|
35365
35389
|
var init_errors3 = __esm({
|
|
35366
35390
|
"node_modules/@chrischall/mcp-utils/dist/errors/index.js"() {
|
|
35367
35391
|
DEFAULT_ERROR_MESSAGE_MAX = 500;
|
|
35392
|
+
MCP_TOOL_ERROR_KINDS = /* @__PURE__ */ new Set([
|
|
35393
|
+
"no_credential",
|
|
35394
|
+
"credential_rejected",
|
|
35395
|
+
"edge_blocked",
|
|
35396
|
+
"session_expired",
|
|
35397
|
+
"verification_pending",
|
|
35398
|
+
"timeout",
|
|
35399
|
+
"http",
|
|
35400
|
+
"transport",
|
|
35401
|
+
"too_large",
|
|
35402
|
+
"unknown"
|
|
35403
|
+
]);
|
|
35368
35404
|
McpToolError = class extends Error {
|
|
35369
35405
|
/** Actionable remediation text, when one applies. */
|
|
35370
35406
|
hint;
|
|
35407
|
+
/** The upstream HTTP status this failure came from, when known. */
|
|
35408
|
+
status;
|
|
35409
|
+
/** What kind of failure this is, when the thrower knows. */
|
|
35410
|
+
kind;
|
|
35371
35411
|
constructor(message, opts) {
|
|
35372
35412
|
super(message, opts?.cause !== void 0 ? { cause: opts.cause } : void 0);
|
|
35373
35413
|
this.name = "McpToolError";
|
|
35374
35414
|
if (opts?.hint !== void 0)
|
|
35375
35415
|
this.hint = opts.hint;
|
|
35416
|
+
if (opts?.status !== void 0)
|
|
35417
|
+
this.status = opts.status;
|
|
35418
|
+
if (opts?.kind !== void 0)
|
|
35419
|
+
this.kind = opts.kind;
|
|
35376
35420
|
Object.setPrototypeOf(this, new.target.prototype);
|
|
35377
35421
|
}
|
|
35378
35422
|
};
|
|
35423
|
+
DECLARATION_CAUSE_DEPTH = 4;
|
|
35424
|
+
UPSTREAM_BODY_PHRASE = {
|
|
35425
|
+
"non-json": "a non-JSON response",
|
|
35426
|
+
empty: "an empty body",
|
|
35427
|
+
null: "null",
|
|
35428
|
+
array: "an array",
|
|
35429
|
+
object: "an object",
|
|
35430
|
+
string: "a string",
|
|
35431
|
+
number: "a number",
|
|
35432
|
+
boolean: "a boolean"
|
|
35433
|
+
};
|
|
35434
|
+
UpstreamFormatError = class extends McpToolError {
|
|
35435
|
+
/** What the body turned out to be. */
|
|
35436
|
+
received;
|
|
35437
|
+
/** The shape the caller required, when it required one. */
|
|
35438
|
+
expected;
|
|
35439
|
+
/** The response `Content-Type`, when known. (`status` is the base class's.) */
|
|
35440
|
+
contentType;
|
|
35441
|
+
constructor(details) {
|
|
35442
|
+
const service = details.service ?? "The upstream service";
|
|
35443
|
+
const request = details.method !== void 0 && details.path !== void 0 ? `${details.method.toUpperCase()} ${details.path}` : void 0;
|
|
35444
|
+
const meta3 = [
|
|
35445
|
+
details.status !== void 0 ? `HTTP ${details.status}` : void 0,
|
|
35446
|
+
details.contentType || void 0
|
|
35447
|
+
].filter((p) => p !== void 0);
|
|
35448
|
+
const what = details.expected !== void 0 ? `${UPSTREAM_BODY_PHRASE[details.received]} where a JSON ${details.expected} was expected` + (request ? ` for ${request}` : "") : `${UPSTREAM_BODY_PHRASE[details.received]}` + (request ? ` to ${request}` : "");
|
|
35449
|
+
const message = `${service} returned ${what}${meta3.length > 0 ? ` (${meta3.join(", ")})` : ""}.`;
|
|
35450
|
+
const hint = details.received === "non-json" ? "The service answered with something other than JSON, usually a sign-in page or interstitial, or its API has changed. Check the session, then retry." : "The service answered with a body of the wrong shape: the resource may be missing or its API may have changed.";
|
|
35451
|
+
super(truncateErrorMessage(message), {
|
|
35452
|
+
hint,
|
|
35453
|
+
cause: details.cause,
|
|
35454
|
+
...details.status !== void 0 ? { status: details.status } : {}
|
|
35455
|
+
});
|
|
35456
|
+
this.name = "UpstreamFormatError";
|
|
35457
|
+
this.received = details.received;
|
|
35458
|
+
if (details.expected !== void 0)
|
|
35459
|
+
this.expected = details.expected;
|
|
35460
|
+
if (details.contentType)
|
|
35461
|
+
this.contentType = details.contentType;
|
|
35462
|
+
}
|
|
35463
|
+
};
|
|
35379
35464
|
HEADER_NAME_RUN = "[a-z0-9_-]{0,40}";
|
|
35380
35465
|
BEARER_RE = /(bearer\s+)[A-Za-z0-9._~+/=-]{8,}/gi;
|
|
35381
35466
|
JWT_RE = /(?<![A-Za-z0-9_-])[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{8,}(?![A-Za-z0-9_-])/g;
|
|
@@ -35397,7 +35482,7 @@ var init_errors3 = __esm({
|
|
|
35397
35482
|
// webhook signing secret (Stripe-style)
|
|
35398
35483
|
].map((p) => `\\b${p}`).join("|"), "g");
|
|
35399
35484
|
GOOGLE_OAUTH_TOKEN_RE = /(?<![A-Za-z0-9+/])(?:ya29\.|1\/\/)[A-Za-z0-9._-]{8,}/g;
|
|
35400
|
-
QUERY_SECRET_RE = new RegExp(`([?&](?:(?:access|refresh|id|auth|session|csrf|xsrf)[_-]?token|client[_-]?secret|api[_-]?secret|api[_-]?key|password|passwd|signature|token|key|sig|(?:php|j)?sess(?:ion)?[_-]?id|sid|x[-_]${HEADER_NAME_RUN}?(?:api[-_]?key|token|secret|auth)${HEADER_NAME_RUN})=)[^&#\\s"'<>\`]+`, "gi");
|
|
35485
|
+
QUERY_SECRET_RE = new RegExp(`([?&](?:(?:access|refresh|id|auth|session|csrf|xsrf)[_-]?token|client[_-]?secret|api[_-]?secret|api[_-]?key|password|passwd|signature|token|key|sig|(?:code[_-]?)?verifier|(?:php|j)?sess(?:ion)?[_-]?id|sid|x[-_]${HEADER_NAME_RUN}?(?:api[-_]?key|token|secret|auth)${HEADER_NAME_RUN})=)[^&#\\s"'<>\`]+`, "gi");
|
|
35401
35486
|
OAUTH_CODE_RE = /([?&]code=)(?=[^&#\s"'<>`]{16,})[^&#\s"'<>`]+/gi;
|
|
35402
35487
|
FORM_PASSWORD_RE = /((?:^|[\s,;:"'(\[{])(?:password|passwd)=)[^&#\s"'<>`]+/gim;
|
|
35403
35488
|
HEADER_SECRET_RE = new RegExp(`(\\b(?:x[-_]${HEADER_NAME_RUN}?(?:api[-_]?key|token|secret|auth)${HEADER_NAME_RUN}|api[-_]?key)\\s*:\\s*)[^\\s,;"'<>\`]+`, "gi");
|
|
@@ -35408,6 +35493,8 @@ var init_errors3 = __esm({
|
|
|
35408
35493
|
"api[_-]?secret",
|
|
35409
35494
|
"(?:x[_-])?api[_-]?key",
|
|
35410
35495
|
"private[_-]?key",
|
|
35496
|
+
"code[_-]?verifier",
|
|
35497
|
+
// PKCE; a bare `verifier` key is too generic for JSON
|
|
35411
35498
|
`x[_-]${HEADER_NAME_RUN}?(?:api[_-]?key|token|secret|auth)${HEADER_NAME_RUN}`,
|
|
35412
35499
|
// bounded like HEADER_SECRET_RE
|
|
35413
35500
|
"(?:proxy-)?authorization",
|
|
@@ -35529,6 +35616,11 @@ function textResult(data) {
|
|
|
35529
35616
|
content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
|
|
35530
35617
|
};
|
|
35531
35618
|
}
|
|
35619
|
+
function imageResult(base643, mimeType) {
|
|
35620
|
+
return {
|
|
35621
|
+
content: [{ type: "image", data: base643, mimeType }]
|
|
35622
|
+
};
|
|
35623
|
+
}
|
|
35532
35624
|
function errorResult(message) {
|
|
35533
35625
|
return {
|
|
35534
35626
|
content: [{ type: "text", text: redactSecrets(message) }],
|
|
@@ -35904,7 +35996,7 @@ function readEnvVar(key, opts = {}) {
|
|
|
35904
35996
|
if (typeof raw === "string") {
|
|
35905
35997
|
const trimmed = raw.trim();
|
|
35906
35998
|
if (trimmed.length > 0 && trimmed !== "undefined" && trimmed !== "null" && !PLACEHOLDER_RE.test(trimmed)) {
|
|
35907
|
-
return trimmed;
|
|
35999
|
+
return opts.trim === false ? raw : trimmed;
|
|
35908
36000
|
}
|
|
35909
36001
|
}
|
|
35910
36002
|
return opts.default;
|
|
@@ -36141,12 +36233,19 @@ function boundArgs(args) {
|
|
|
36141
36233
|
}
|
|
36142
36234
|
function confirmationFromEnv(options) {
|
|
36143
36235
|
const { tool, account, confirmToken, subject, instruction, args, env = process.env, spent: callerSpent, ...rest } = options;
|
|
36236
|
+
if (args === void 0 || args === null) {
|
|
36237
|
+
throw new TypeError(`confirmationFromEnv: ${tool} passed no args; pass the tool's validated arguments ({} for a tool with none).`);
|
|
36238
|
+
}
|
|
36144
36239
|
const spent = callerSpent ?? spentTokenStoreFromEnv(env);
|
|
36145
36240
|
const mode = readConfirmMode(env);
|
|
36146
36241
|
const ttlSeconds = readConfirmTtl(env);
|
|
36147
36242
|
const elicitationOff = readConfirmElicitation(env) === "off";
|
|
36148
|
-
const bound =
|
|
36149
|
-
const
|
|
36243
|
+
const bound = boundArgs(args);
|
|
36244
|
+
const elicitationBound = account === void 0 ? { args: bound } : { account, args: bound };
|
|
36245
|
+
const bindingAdded = rest.binding ? rest : {
|
|
36246
|
+
...rest,
|
|
36247
|
+
binding: { key: confirmKeyFromEnv(env), args: elicitationBound, ttlSeconds: ttlSeconds ?? DEFAULT_TTL_SECONDS2 }
|
|
36248
|
+
};
|
|
36150
36249
|
const confirmation = elicitationOff ? { ...bindingAdded, elicitation: false } : bindingAdded;
|
|
36151
36250
|
if (mode === "refuse" || ttlSeconds === void 0) {
|
|
36152
36251
|
const reason = mode === "refuse" ? REFUSE_HINT : BAD_TTL_HINT;
|
|
@@ -36156,7 +36255,7 @@ function confirmationFromEnv(options) {
|
|
|
36156
36255
|
unsupportedNote: confirmation.unsupportedNote ? `${confirmation.unsupportedNote} ${hint}` : hint
|
|
36157
36256
|
};
|
|
36158
36257
|
}
|
|
36159
|
-
const boundSubject =
|
|
36258
|
+
const boundSubject = async () => {
|
|
36160
36259
|
const read = await subject();
|
|
36161
36260
|
if ("content" in read)
|
|
36162
36261
|
return read;
|
|
@@ -36244,7 +36343,7 @@ async function confirmWrite(ctx, options) {
|
|
|
36244
36343
|
...options.unsupportedNote === void 0 ? {} : { unsupportedNote: options.unsupportedNote },
|
|
36245
36344
|
...options.confirmationLabel === void 0 ? {} : { confirmationLabel: options.confirmationLabel },
|
|
36246
36345
|
tool,
|
|
36247
|
-
|
|
36346
|
+
account,
|
|
36248
36347
|
confirmToken,
|
|
36249
36348
|
args: bound,
|
|
36250
36349
|
subject: () => ({ target, ...revision === void 0 ? {} : { revision }, payload: bound, preview }),
|
|
@@ -36272,12 +36371,14 @@ var init_merge_write = __esm({
|
|
|
36272
36371
|
|
|
36273
36372
|
// node_modules/@chrischall/mcp-utils/dist/server/index.js
|
|
36274
36373
|
function hintResultOrRethrow(err) {
|
|
36374
|
+
if (err instanceof ProtocolError && err.code === ProtocolErrorCode.UrlElicitationRequired)
|
|
36375
|
+
throw err;
|
|
36275
36376
|
if (err instanceof McpToolError && err.hint) {
|
|
36276
36377
|
return errorResult(`${err.message}
|
|
36277
36378
|
|
|
36278
36379
|
Hint: ${err.hint}`);
|
|
36279
36380
|
}
|
|
36280
|
-
|
|
36381
|
+
return errorResult(messageOf(err));
|
|
36281
36382
|
}
|
|
36282
36383
|
function mcpRequestFrom(args) {
|
|
36283
36384
|
const ctx = args.at(-1);
|
|
@@ -36496,6 +36597,7 @@ var init_write = __esm({
|
|
|
36496
36597
|
var init_output = __esm({
|
|
36497
36598
|
"node_modules/@chrischall/mcp-utils/dist/fs/output.js"() {
|
|
36498
36599
|
init_config();
|
|
36600
|
+
init_errors3();
|
|
36499
36601
|
init_confine();
|
|
36500
36602
|
init_write();
|
|
36501
36603
|
}
|
|
@@ -36576,6 +36678,7 @@ var init_scrape = __esm({
|
|
|
36576
36678
|
// node_modules/@chrischall/mcp-utils/dist/http/throttle.js
|
|
36577
36679
|
var init_throttle = __esm({
|
|
36578
36680
|
"node_modules/@chrischall/mcp-utils/dist/http/throttle.js"() {
|
|
36681
|
+
init_cancel();
|
|
36579
36682
|
}
|
|
36580
36683
|
});
|
|
36581
36684
|
|
|
@@ -36601,6 +36704,113 @@ var init_net_atoms = __esm({
|
|
|
36601
36704
|
}
|
|
36602
36705
|
});
|
|
36603
36706
|
|
|
36707
|
+
// node_modules/@chrischall/mcp-utils/dist/http/body-limit.js
|
|
36708
|
+
async function readBytesCapped(res, maxBytes, service, bounded) {
|
|
36709
|
+
if (maxBytes !== void 0) {
|
|
36710
|
+
const declared = Number(res.headers.get("content-length") ?? NaN);
|
|
36711
|
+
if (Number.isFinite(declared) && declared > maxBytes) {
|
|
36712
|
+
cancelStream(res.body);
|
|
36713
|
+
throw new ResponseTooLargeError(service, maxBytes);
|
|
36714
|
+
}
|
|
36715
|
+
}
|
|
36716
|
+
if (!res.body)
|
|
36717
|
+
return new Uint8Array(0);
|
|
36718
|
+
const reader = res.body.getReader();
|
|
36719
|
+
const chunks = [];
|
|
36720
|
+
let total = 0;
|
|
36721
|
+
try {
|
|
36722
|
+
for (; ; ) {
|
|
36723
|
+
const { done, value } = await bounded(reader.read());
|
|
36724
|
+
if (done)
|
|
36725
|
+
break;
|
|
36726
|
+
total += value.byteLength;
|
|
36727
|
+
if (maxBytes !== void 0 && total > maxBytes)
|
|
36728
|
+
throw new ResponseTooLargeError(service, maxBytes);
|
|
36729
|
+
chunks.push(value);
|
|
36730
|
+
}
|
|
36731
|
+
} catch (err) {
|
|
36732
|
+
reader.cancel().catch(() => {
|
|
36733
|
+
});
|
|
36734
|
+
throw err;
|
|
36735
|
+
}
|
|
36736
|
+
const out = new Uint8Array(total);
|
|
36737
|
+
let offset = 0;
|
|
36738
|
+
for (const c of chunks) {
|
|
36739
|
+
out.set(c, offset);
|
|
36740
|
+
offset += c.byteLength;
|
|
36741
|
+
}
|
|
36742
|
+
return out;
|
|
36743
|
+
}
|
|
36744
|
+
function cancelStream(body) {
|
|
36745
|
+
try {
|
|
36746
|
+
body?.cancel().catch(() => {
|
|
36747
|
+
});
|
|
36748
|
+
} catch {
|
|
36749
|
+
}
|
|
36750
|
+
}
|
|
36751
|
+
var ResponseTooLargeError;
|
|
36752
|
+
var init_body_limit = __esm({
|
|
36753
|
+
"node_modules/@chrischall/mcp-utils/dist/http/body-limit.js"() {
|
|
36754
|
+
init_errors3();
|
|
36755
|
+
ResponseTooLargeError = class extends McpToolError {
|
|
36756
|
+
maxBytes;
|
|
36757
|
+
constructor(service, maxBytes) {
|
|
36758
|
+
super(`Response from ${service} is larger than the ${maxBytes}-byte limit.`, {
|
|
36759
|
+
hint: "Raise maxBytes (or the client's maxResponseBytes) if a response this large is expected.",
|
|
36760
|
+
kind: "too_large"
|
|
36761
|
+
});
|
|
36762
|
+
this.name = "ResponseTooLargeError";
|
|
36763
|
+
this.maxBytes = maxBytes;
|
|
36764
|
+
}
|
|
36765
|
+
};
|
|
36766
|
+
}
|
|
36767
|
+
});
|
|
36768
|
+
|
|
36769
|
+
// node_modules/@chrischall/mcp-utils/dist/http/fetch-bounded.js
|
|
36770
|
+
var init_fetch_bounded = __esm({
|
|
36771
|
+
"node_modules/@chrischall/mcp-utils/dist/http/fetch-bounded.js"() {
|
|
36772
|
+
init_cancel();
|
|
36773
|
+
init_errors3();
|
|
36774
|
+
init_body_limit();
|
|
36775
|
+
init_http();
|
|
36776
|
+
}
|
|
36777
|
+
});
|
|
36778
|
+
|
|
36779
|
+
// node_modules/@chrischall/mcp-utils/dist/http/url-safety.js
|
|
36780
|
+
function isDotSegment(segment) {
|
|
36781
|
+
const s = segment.replace(/%2e/gi, ".");
|
|
36782
|
+
return s === "." || s === "..";
|
|
36783
|
+
}
|
|
36784
|
+
function pathPartOf(path) {
|
|
36785
|
+
const end = path.search(/[?#]/);
|
|
36786
|
+
return end === -1 ? path : path.slice(0, end);
|
|
36787
|
+
}
|
|
36788
|
+
function findPathHazard(path) {
|
|
36789
|
+
const pathPart = pathPartOf(path);
|
|
36790
|
+
if (/[\x00-\x1f\x7f]/.test(pathPart))
|
|
36791
|
+
return "control character";
|
|
36792
|
+
if (pathPart.endsWith(" "))
|
|
36793
|
+
return "trailing space";
|
|
36794
|
+
if (pathPart.includes("\\"))
|
|
36795
|
+
return "backslash";
|
|
36796
|
+
if (pathPart.split("/").some(isDotSegment))
|
|
36797
|
+
return "dot segment";
|
|
36798
|
+
return void 0;
|
|
36799
|
+
}
|
|
36800
|
+
var RedirectRefusedError;
|
|
36801
|
+
var init_url_safety = __esm({
|
|
36802
|
+
"node_modules/@chrischall/mcp-utils/dist/http/url-safety.js"() {
|
|
36803
|
+
init_errors3();
|
|
36804
|
+
init_config();
|
|
36805
|
+
RedirectRefusedError = class extends McpToolError {
|
|
36806
|
+
constructor(message) {
|
|
36807
|
+
super(message);
|
|
36808
|
+
this.name = "RedirectRefusedError";
|
|
36809
|
+
}
|
|
36810
|
+
};
|
|
36811
|
+
}
|
|
36812
|
+
});
|
|
36813
|
+
|
|
36604
36814
|
// node_modules/@chrischall/mcp-utils/dist/internal/retry-after.js
|
|
36605
36815
|
function retryAfterToMs(header, now = Date.now()) {
|
|
36606
36816
|
if (header == null)
|
|
@@ -36621,6 +36831,14 @@ var init_retry_after = __esm({
|
|
|
36621
36831
|
});
|
|
36622
36832
|
|
|
36623
36833
|
// node_modules/@chrischall/mcp-utils/dist/http/index.js
|
|
36834
|
+
function cancelBody(res) {
|
|
36835
|
+
try {
|
|
36836
|
+
const body = res.body;
|
|
36837
|
+
body?.cancel?.()?.catch(() => {
|
|
36838
|
+
});
|
|
36839
|
+
} catch {
|
|
36840
|
+
}
|
|
36841
|
+
}
|
|
36624
36842
|
function headerOf(headers, name) {
|
|
36625
36843
|
if (headers === void 0)
|
|
36626
36844
|
return void 0;
|
|
@@ -36646,6 +36864,49 @@ function detectEdgeBlock(input2) {
|
|
|
36646
36864
|
}
|
|
36647
36865
|
return null;
|
|
36648
36866
|
}
|
|
36867
|
+
function jsonKind(value) {
|
|
36868
|
+
if (value === null)
|
|
36869
|
+
return "null";
|
|
36870
|
+
if (Array.isArray(value))
|
|
36871
|
+
return "array";
|
|
36872
|
+
return typeof value;
|
|
36873
|
+
}
|
|
36874
|
+
function parseJsonBody(text, opts = {}) {
|
|
36875
|
+
const contentType = opts.contentType ?? headerOf(opts.headers, "content-type");
|
|
36876
|
+
const details = {
|
|
36877
|
+
service: opts.service,
|
|
36878
|
+
method: opts.method,
|
|
36879
|
+
path: opts.path,
|
|
36880
|
+
status: opts.status,
|
|
36881
|
+
contentType,
|
|
36882
|
+
expected: opts.expect
|
|
36883
|
+
};
|
|
36884
|
+
if (text.trim().length === 0) {
|
|
36885
|
+
if (opts.expect === void 0)
|
|
36886
|
+
return void 0;
|
|
36887
|
+
throw new UpstreamFormatError({ ...details, received: "empty" });
|
|
36888
|
+
}
|
|
36889
|
+
let value;
|
|
36890
|
+
try {
|
|
36891
|
+
value = JSON.parse(text);
|
|
36892
|
+
} catch (cause) {
|
|
36893
|
+
const edge = detectEdgeBlock({ body: text, headers: opts.headers, status: opts.status });
|
|
36894
|
+
if (edge) {
|
|
36895
|
+
throw new EdgeBlockedError(opts.status ?? 200, edge.vendor, {
|
|
36896
|
+
service: opts.service ?? "the upstream service",
|
|
36897
|
+
method: opts.method,
|
|
36898
|
+
path: opts.path
|
|
36899
|
+
});
|
|
36900
|
+
}
|
|
36901
|
+
throw new UpstreamFormatError({ ...details, expected: void 0, received: "non-json", cause });
|
|
36902
|
+
}
|
|
36903
|
+
if (opts.expect !== void 0) {
|
|
36904
|
+
const kind = jsonKind(value);
|
|
36905
|
+
if (kind !== opts.expect)
|
|
36906
|
+
throw new UpstreamFormatError({ ...details, received: kind });
|
|
36907
|
+
}
|
|
36908
|
+
return value;
|
|
36909
|
+
}
|
|
36649
36910
|
function httpError(res, text, method, path, service) {
|
|
36650
36911
|
const edge = detectEdgeBlock({ body: text, headers: res.headers, status: res.status });
|
|
36651
36912
|
if (edge)
|
|
@@ -36696,13 +36957,26 @@ function createApiClient(opts) {
|
|
|
36696
36957
|
const sleep3 = opts.sleep ?? defaultSleep;
|
|
36697
36958
|
const unauthorized = () => opts.onUnauthorized ? opts.onUnauthorized() : new UnauthorizedError(service);
|
|
36698
36959
|
const retryStatuses = retry.statuses ?? [429];
|
|
36699
|
-
const timeoutMs = opts.timeout;
|
|
36960
|
+
const timeoutMs = opts.timeout === false ? 0 : opts.timeout ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
36961
|
+
const redirectMode = opts.redirect;
|
|
36962
|
+
const maxRedirects = opts.maxRedirects ?? DEFAULT_MAX_REDIRECTS;
|
|
36963
|
+
const reportWriteOutcome = opts.writeOutcomeUnknown !== false;
|
|
36964
|
+
const defaultMaxBytes = checkMaxBytes("maxResponseBytes", opts.maxResponseBytes);
|
|
36965
|
+
if (!Number.isSafeInteger(maxRedirects) || maxRedirects < 0) {
|
|
36966
|
+
throw new TypeError(`createApiClient: maxRedirects must be a non-negative integer, got ${String(maxRedirects)}.`);
|
|
36967
|
+
}
|
|
36700
36968
|
const authHeader = (token) => !token ? {} : opts.tokenHeader ? { [opts.tokenHeader]: token } : { Authorization: `Bearer ${token}` };
|
|
36969
|
+
const transportFailures = /* @__PURE__ */ new WeakSet();
|
|
36970
|
+
const markTransportFailure = (err) => {
|
|
36971
|
+
if (typeof err === "object" && err !== null && err.name !== "AbortError") {
|
|
36972
|
+
transportFailures.add(err);
|
|
36973
|
+
}
|
|
36974
|
+
};
|
|
36701
36975
|
async function withTimeout(run) {
|
|
36702
|
-
if (timeoutMs
|
|
36976
|
+
if (timeoutMs <= 0) {
|
|
36703
36977
|
const res2 = await run(withAmbientCancellation(void 0));
|
|
36704
36978
|
return { res: res2, done: () => {
|
|
36705
|
-
}, expired: void 0 };
|
|
36979
|
+
}, expired: void 0, timedOut: () => false };
|
|
36706
36980
|
}
|
|
36707
36981
|
const controller = new AbortController();
|
|
36708
36982
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
@@ -36723,16 +36997,11 @@ function createApiClient(opts) {
|
|
|
36723
36997
|
});
|
|
36724
36998
|
expired.catch(() => {
|
|
36725
36999
|
});
|
|
36726
|
-
return { res, done: () => clearTimeout(timer), expired };
|
|
37000
|
+
return { res, done: () => clearTimeout(timer), expired, timedOut: () => controller.signal.aborted };
|
|
36727
37001
|
}
|
|
36728
37002
|
function discard(attempt) {
|
|
36729
37003
|
attempt.done();
|
|
36730
|
-
|
|
36731
|
-
const body = attempt.res.body;
|
|
36732
|
-
body?.cancel?.()?.catch(() => {
|
|
36733
|
-
});
|
|
36734
|
-
} catch {
|
|
36735
|
-
}
|
|
37004
|
+
cancelBody(attempt.res);
|
|
36736
37005
|
}
|
|
36737
37006
|
async function readBody(attempt, read) {
|
|
36738
37007
|
try {
|
|
@@ -36740,15 +37009,40 @@ function createApiClient(opts) {
|
|
|
36740
37009
|
return await read(attempt.res);
|
|
36741
37010
|
return await Promise.race([read(attempt.res), attempt.expired]);
|
|
36742
37011
|
} catch (err) {
|
|
36743
|
-
|
|
36744
|
-
|
|
36745
|
-
|
|
37012
|
+
const timeout = err instanceof RequestTimeoutError || err instanceof Error && err.name === "AbortError" && attempt.timedOut();
|
|
37013
|
+
if (timeout) {
|
|
37014
|
+
cancelBody(attempt.res);
|
|
37015
|
+
throw err instanceof RequestTimeoutError ? err : new RequestTimeoutError(service, timeoutMs);
|
|
36746
37016
|
}
|
|
37017
|
+
if (!(err instanceof ResponseTooLargeError))
|
|
37018
|
+
markTransportFailure(err);
|
|
36747
37019
|
throw err;
|
|
36748
37020
|
} finally {
|
|
36749
37021
|
attempt.done();
|
|
36750
37022
|
}
|
|
36751
37023
|
}
|
|
37024
|
+
function readBytes(attempt, maxBytes) {
|
|
37025
|
+
if (maxBytes === void 0)
|
|
37026
|
+
return readBody(attempt, async (r) => new Uint8Array(await r.arrayBuffer()));
|
|
37027
|
+
const race = (p) => attempt.expired ? Promise.race([p, attempt.expired]) : p;
|
|
37028
|
+
return readBody(attempt, (r) => readBytesCapped(r, maxBytes, service, race));
|
|
37029
|
+
}
|
|
37030
|
+
async function readText(attempt, maxBytes) {
|
|
37031
|
+
if (maxBytes === void 0)
|
|
37032
|
+
return readBody(attempt, (r) => r.text());
|
|
37033
|
+
return new TextDecoder().decode(await readBytes(attempt, maxBytes));
|
|
37034
|
+
}
|
|
37035
|
+
function readErrorText(attempt, maxBytes) {
|
|
37036
|
+
return readText(attempt, maxBytes).catch((err) => {
|
|
37037
|
+
if (err instanceof ResponseTooLargeError)
|
|
37038
|
+
return "";
|
|
37039
|
+
throw err;
|
|
37040
|
+
});
|
|
37041
|
+
}
|
|
37042
|
+
function scanText(attempt, maxBytes) {
|
|
37043
|
+
return readText(attempt, maxBytes).catch(() => "");
|
|
37044
|
+
}
|
|
37045
|
+
const capOf = (opt) => opt.maxBytes !== void 0 ? checkMaxBytes("maxBytes", opt.maxBytes) : defaultMaxBytes;
|
|
36752
37046
|
const baseParts = (() => {
|
|
36753
37047
|
try {
|
|
36754
37048
|
const u = new URL(base);
|
|
@@ -36759,6 +37053,14 @@ function createApiClient(opts) {
|
|
|
36759
37053
|
})();
|
|
36760
37054
|
const baseOrigin = baseParts?.origin;
|
|
36761
37055
|
function resolveUrl(path, query) {
|
|
37056
|
+
const hazard = findPathHazard(path);
|
|
37057
|
+
if (hazard !== void 0) {
|
|
37058
|
+
throw new Error(
|
|
37059
|
+
// Quote only the path part: this fires on hostile input, and the
|
|
37060
|
+
// query may carry values that do not belong in a log line.
|
|
37061
|
+
`Refusing request to ${service}: path ${JSON.stringify(path.split(/[?#]/, 1)[0])} contains a ${hazard}, which URL normalisation would rewrite. Build paths with apiPath\`...\` so each value is one encoded segment.`
|
|
37062
|
+
);
|
|
37063
|
+
}
|
|
36762
37064
|
const url2 = `${base}${path}${query}`;
|
|
36763
37065
|
if (baseOrigin === void 0)
|
|
36764
37066
|
return url2;
|
|
@@ -36773,23 +37075,66 @@ function createApiClient(opts) {
|
|
|
36773
37075
|
}
|
|
36774
37076
|
return url2;
|
|
36775
37077
|
}
|
|
36776
|
-
|
|
37078
|
+
function sameOriginHop(location, from) {
|
|
37079
|
+
let next;
|
|
37080
|
+
try {
|
|
37081
|
+
next = new URL(location, from);
|
|
37082
|
+
} catch {
|
|
37083
|
+
throw new RedirectRefusedError(`Refusing redirect from ${service}: the Location header is not a valid URL.`);
|
|
37084
|
+
}
|
|
37085
|
+
if (baseOrigin === void 0 || next.origin !== baseOrigin || next.username !== baseParts?.username || next.password !== baseParts?.password) {
|
|
37086
|
+
throw new RedirectRefusedError(`Refusing redirect from ${service} to ${next.origin}: it leaves the base origin ${baseOrigin ?? "(unparseable)"}, and following it would re-send the credential there.`);
|
|
37087
|
+
}
|
|
37088
|
+
return next.href;
|
|
37089
|
+
}
|
|
37090
|
+
async function send(method, path, opt, dispatch) {
|
|
36777
37091
|
const { body: reqBody, contentType } = encodeBody(opt);
|
|
36778
37092
|
const query = opt.query ? buildQueryString(opt.query) : "";
|
|
36779
37093
|
const url2 = resolveUrl(path, query);
|
|
36780
37094
|
const bodyInit = reqBody !== void 0 ? { body: reqBody } : {};
|
|
36781
|
-
const
|
|
36782
|
-
|
|
36783
|
-
|
|
36784
|
-
|
|
36785
|
-
|
|
36786
|
-
|
|
36787
|
-
|
|
36788
|
-
|
|
36789
|
-
|
|
36790
|
-
|
|
36791
|
-
|
|
36792
|
-
|
|
37095
|
+
const fetchHop = (hopUrl, hopMethod, withBody, token, signal) => {
|
|
37096
|
+
if (!signal?.aborted)
|
|
37097
|
+
dispatch.sent = true;
|
|
37098
|
+
return doFetch(hopUrl, {
|
|
37099
|
+
method: hopMethod,
|
|
37100
|
+
headers: {
|
|
37101
|
+
Accept: "application/json",
|
|
37102
|
+
...withBody && contentType !== void 0 ? { "Content-Type": contentType } : {},
|
|
37103
|
+
...opts.baseHeaders,
|
|
37104
|
+
...authHeader(token || void 0),
|
|
37105
|
+
...opt.headers
|
|
37106
|
+
},
|
|
37107
|
+
...signal ? { signal } : {},
|
|
37108
|
+
...redirectMode !== void 0 ? { redirect: redirectMode === "same-origin" ? "manual" : redirectMode } : {},
|
|
37109
|
+
...withBody ? bodyInit : {}
|
|
37110
|
+
}).catch((err) => {
|
|
37111
|
+
markTransportFailure(err);
|
|
37112
|
+
throw err;
|
|
37113
|
+
});
|
|
37114
|
+
};
|
|
37115
|
+
const fetchWith = async (token, signal) => {
|
|
37116
|
+
if (redirectMode !== "same-origin")
|
|
37117
|
+
return fetchHop(url2, method, true, token, signal);
|
|
37118
|
+
let hopUrl = url2;
|
|
37119
|
+
let hopMethod = method;
|
|
37120
|
+
let withBody = true;
|
|
37121
|
+
for (let hops = 0; ; hops += 1) {
|
|
37122
|
+
const res = await fetchHop(hopUrl, hopMethod, withBody, token, signal);
|
|
37123
|
+
const location = REDIRECT_STATUSES.has(res.status) ? responseHeader(res, "location") : void 0;
|
|
37124
|
+
if (location === void 0 || location === null)
|
|
37125
|
+
return res;
|
|
37126
|
+
cancelBody(res);
|
|
37127
|
+
if (hops >= maxRedirects) {
|
|
37128
|
+
throw new RedirectRefusedError(`Refusing request to ${service}: too many redirects (more than ${maxRedirects}).`);
|
|
37129
|
+
}
|
|
37130
|
+
hopUrl = sameOriginHop(location, hopUrl);
|
|
37131
|
+
if (res.status === 303 || (res.status === 301 || res.status === 302) && hopMethod === "POST") {
|
|
37132
|
+
if (hopMethod !== "HEAD")
|
|
37133
|
+
hopMethod = "GET";
|
|
37134
|
+
withBody = false;
|
|
37135
|
+
}
|
|
37136
|
+
}
|
|
37137
|
+
};
|
|
36793
37138
|
const once = () => withTimeout((signal) => opts.tokenManager ? opts.tokenManager.withAuth((token) => fetchWith(token, signal)) : (async () => fetchWith(await opts.getToken?.() || void 0, signal))());
|
|
36794
37139
|
let attempt = 0;
|
|
36795
37140
|
for (; ; ) {
|
|
@@ -36808,20 +37153,20 @@ function createApiClient(opts) {
|
|
|
36808
37153
|
return current;
|
|
36809
37154
|
}
|
|
36810
37155
|
}
|
|
36811
|
-
async function unauthorizedOrEdge(attempt, method, path) {
|
|
37156
|
+
async function unauthorizedOrEdge(attempt, method, path, maxBytes) {
|
|
36812
37157
|
const res = attempt.res;
|
|
36813
37158
|
let text = "";
|
|
36814
37159
|
if (/json/i.test(responseHeader(res, "content-type") ?? "")) {
|
|
36815
37160
|
discard(attempt);
|
|
36816
37161
|
} else {
|
|
36817
|
-
text = await
|
|
37162
|
+
text = await scanText(attempt, maxBytes);
|
|
36818
37163
|
}
|
|
36819
37164
|
const edge = detectEdgeBlock({ body: text, headers: res.headers, status: res.status });
|
|
36820
37165
|
if (edge)
|
|
36821
37166
|
return new EdgeBlockedError(res.status, edge.vendor, { service, method, path });
|
|
36822
37167
|
return unauthorized();
|
|
36823
37168
|
}
|
|
36824
|
-
async function rateLimitedError(attempt, method, path) {
|
|
37169
|
+
async function rateLimitedError(attempt, method, path, maxBytes) {
|
|
36825
37170
|
const res = attempt.res;
|
|
36826
37171
|
const retryAfter = responseHeader(res, "retry-after");
|
|
36827
37172
|
const retryAfterMs = retryAfterToMs(retryAfter);
|
|
@@ -36833,61 +37178,92 @@ function createApiClient(opts) {
|
|
|
36833
37178
|
if (/json/i.test(responseHeader(res, "content-type") ?? "")) {
|
|
36834
37179
|
discard(attempt);
|
|
36835
37180
|
} else {
|
|
36836
|
-
text = await
|
|
37181
|
+
text = await scanText(attempt, maxBytes);
|
|
36837
37182
|
}
|
|
36838
37183
|
const edgeBlock = detectEdgeBlock({ body: text, headers: res.headers, status: res.status });
|
|
36839
37184
|
return opts.onRateLimited({ status: res.status, retryAfter, retryAfterMs, edgeBlock, method, path });
|
|
36840
37185
|
}
|
|
36841
|
-
async function
|
|
36842
|
-
const
|
|
37186
|
+
async function guardWrite(method, opt, call) {
|
|
37187
|
+
const dispatch = { sent: false };
|
|
37188
|
+
try {
|
|
37189
|
+
return await call(dispatch);
|
|
37190
|
+
} catch (err) {
|
|
37191
|
+
const write = reportWriteOutcome && opt.idempotent !== true && !SAFE_METHODS.has(method.toUpperCase());
|
|
37192
|
+
if (!write || !dispatch.sent)
|
|
37193
|
+
throw err;
|
|
37194
|
+
if (err instanceof RequestTimeoutError) {
|
|
37195
|
+
throw new WriteOutcomeUnknownError(service, method, {
|
|
37196
|
+
timeoutMs: err.timeoutMs,
|
|
37197
|
+
cause: err,
|
|
37198
|
+
hint: opts.writeOutcomeHint
|
|
37199
|
+
});
|
|
37200
|
+
}
|
|
37201
|
+
if (typeof err === "object" && err !== null && transportFailures.has(err)) {
|
|
37202
|
+
throw new WriteOutcomeUnknownError(service, method, { cause: err, hint: opts.writeOutcomeHint });
|
|
37203
|
+
}
|
|
37204
|
+
throw err;
|
|
37205
|
+
}
|
|
37206
|
+
}
|
|
37207
|
+
const fetchJson = (method, path, opt = {}) => guardWrite(method, opt, (dispatch) => fetchJsonOnce(method, path, opt, dispatch));
|
|
37208
|
+
const fetchHtml = (method, path, opt = {}) => guardWrite(method, opt, (dispatch) => fetchHtmlOnce(method, path, opt, dispatch));
|
|
37209
|
+
const fetchRaw = (method, path, opt = {}) => guardWrite(method, opt, (dispatch) => fetchRawOnce(method, path, opt, dispatch));
|
|
37210
|
+
async function fetchJsonOnce(method, path, opt, dispatch) {
|
|
37211
|
+
const maxBytes = capOf(opt);
|
|
37212
|
+
const attempt = await send(method, path, opt, dispatch);
|
|
36843
37213
|
const res = attempt.res;
|
|
36844
37214
|
if (res.status === 401)
|
|
36845
|
-
throw await unauthorizedOrEdge(attempt, method, path);
|
|
37215
|
+
throw await unauthorizedOrEdge(attempt, method, path, maxBytes);
|
|
36846
37216
|
if (res.status === 429)
|
|
36847
|
-
throw await rateLimitedError(attempt, method, path);
|
|
37217
|
+
throw await rateLimitedError(attempt, method, path, maxBytes);
|
|
36848
37218
|
if (res.status === 204) {
|
|
36849
37219
|
discard(attempt);
|
|
36850
|
-
return
|
|
37220
|
+
return parseJsonBody("", { expect: opt.expect, service, method, path, status: 204 });
|
|
36851
37221
|
}
|
|
36852
|
-
const text = await readBody(attempt, (r) => r.text());
|
|
36853
37222
|
if (!res.ok) {
|
|
36854
|
-
throw httpError(res,
|
|
37223
|
+
throw httpError(res, await readErrorText(attempt, maxBytes), method, path, service);
|
|
36855
37224
|
}
|
|
36856
|
-
|
|
36857
|
-
|
|
36858
|
-
|
|
37225
|
+
const text = await readText(attempt, maxBytes);
|
|
37226
|
+
return parseJsonBody(text, {
|
|
37227
|
+
expect: opt.expect,
|
|
37228
|
+
service,
|
|
37229
|
+
method,
|
|
37230
|
+
path,
|
|
37231
|
+
status: res.status,
|
|
37232
|
+
headers: res.headers
|
|
37233
|
+
});
|
|
36859
37234
|
}
|
|
36860
|
-
async function
|
|
37235
|
+
async function fetchHtmlOnce(method, path, opt, dispatch) {
|
|
37236
|
+
const maxBytes = capOf(opt);
|
|
36861
37237
|
const headers = { Accept: "text/html,*/*", ...opt.headers };
|
|
36862
|
-
const attempt = await send(method, path, { ...opt, headers });
|
|
37238
|
+
const attempt = await send(method, path, { ...opt, headers }, dispatch);
|
|
36863
37239
|
const res = attempt.res;
|
|
36864
37240
|
if (res.status === 401)
|
|
36865
|
-
throw await unauthorizedOrEdge(attempt, method, path);
|
|
37241
|
+
throw await unauthorizedOrEdge(attempt, method, path, maxBytes);
|
|
36866
37242
|
if (res.status === 429)
|
|
36867
|
-
throw await rateLimitedError(attempt, method, path);
|
|
36868
|
-
const text = await readBody(attempt, (r) => r.text());
|
|
37243
|
+
throw await rateLimitedError(attempt, method, path, maxBytes);
|
|
36869
37244
|
if (!res.ok) {
|
|
36870
|
-
throw httpError(res,
|
|
37245
|
+
throw httpError(res, await readErrorText(attempt, maxBytes), method, path, service);
|
|
36871
37246
|
}
|
|
36872
|
-
return
|
|
37247
|
+
return readText(attempt, maxBytes);
|
|
36873
37248
|
}
|
|
36874
|
-
async function
|
|
37249
|
+
async function fetchRawOnce(method, path, opt, dispatch) {
|
|
37250
|
+
const maxBytes = capOf(opt);
|
|
36875
37251
|
const headers = { Accept: "*/*", ...opt.headers };
|
|
36876
|
-
const attempt = await send(method, path, { ...opt, headers });
|
|
37252
|
+
const attempt = await send(method, path, { ...opt, headers }, dispatch);
|
|
36877
37253
|
const res = attempt.res;
|
|
36878
37254
|
if (res.status === 401)
|
|
36879
|
-
throw await unauthorizedOrEdge(attempt, method, path);
|
|
37255
|
+
throw await unauthorizedOrEdge(attempt, method, path, maxBytes);
|
|
36880
37256
|
if (res.status === 429)
|
|
36881
|
-
throw await rateLimitedError(attempt, method, path);
|
|
37257
|
+
throw await rateLimitedError(attempt, method, path, maxBytes);
|
|
36882
37258
|
if (!res.ok) {
|
|
36883
|
-
const text = await
|
|
37259
|
+
const text = await readText(attempt, maxBytes).catch((err) => {
|
|
36884
37260
|
if (err instanceof RequestTimeoutError)
|
|
36885
37261
|
throw err;
|
|
36886
37262
|
return "";
|
|
36887
37263
|
});
|
|
36888
37264
|
throw httpError(res, text, method, path, service);
|
|
36889
37265
|
}
|
|
36890
|
-
const bytes =
|
|
37266
|
+
const bytes = await readBytes(attempt, maxBytes);
|
|
36891
37267
|
return {
|
|
36892
37268
|
status: res.status,
|
|
36893
37269
|
contentType: responseHeader(res, "content-type"),
|
|
@@ -36897,6 +37273,14 @@ function createApiClient(opts) {
|
|
|
36897
37273
|
}
|
|
36898
37274
|
return { fetchJson, fetchHtml, fetchRaw };
|
|
36899
37275
|
}
|
|
37276
|
+
function checkMaxBytes(name, value) {
|
|
37277
|
+
if (value === void 0)
|
|
37278
|
+
return void 0;
|
|
37279
|
+
if (typeof value !== "number" || Number.isNaN(value) || value < 0) {
|
|
37280
|
+
throw new TypeError(`createApiClient: ${name} must be a non-negative number, got ${String(value)}.`);
|
|
37281
|
+
}
|
|
37282
|
+
return value === Infinity ? void 0 : value;
|
|
37283
|
+
}
|
|
36900
37284
|
function buildQueryString(params) {
|
|
36901
37285
|
const parts = [];
|
|
36902
37286
|
for (const [key, value] of Object.entries(params)) {
|
|
@@ -36947,7 +37331,7 @@ function decodeJwtExp(token) {
|
|
|
36947
37331
|
}
|
|
36948
37332
|
return exp;
|
|
36949
37333
|
}
|
|
36950
|
-
var DEFAULT_RETRY, defaultSleep, UnauthorizedError, RateLimitedError, RequestTimeoutError, ApiError, EdgeBlockedError, EDGE_BLOCK_SCAN_MAX, EDGE_BLOCK_MARKERS;
|
|
37334
|
+
var DEFAULT_RETRY, defaultSleep, DEFAULT_REQUEST_TIMEOUT_MS, DEFAULT_MAX_REDIRECTS, REDIRECT_STATUSES, UnauthorizedError, RateLimitedError, RequestTimeoutError, DEFAULT_WRITE_OUTCOME_HINT, WriteOutcomeUnknownError, SAFE_METHODS, ApiError, EdgeBlockedError, EDGE_BLOCK_SCAN_MAX, EDGE_BLOCK_MARKERS;
|
|
36951
37335
|
var init_http = __esm({
|
|
36952
37336
|
"node_modules/@chrischall/mcp-utils/dist/http/index.js"() {
|
|
36953
37337
|
init_headers();
|
|
@@ -36957,12 +37341,21 @@ var init_http = __esm({
|
|
|
36957
37341
|
init_throttle();
|
|
36958
37342
|
init_response_cache();
|
|
36959
37343
|
init_net_atoms();
|
|
37344
|
+
init_fetch_bounded();
|
|
37345
|
+
init_url_safety();
|
|
37346
|
+
init_body_limit();
|
|
36960
37347
|
init_net_atoms();
|
|
37348
|
+
init_url_safety();
|
|
36961
37349
|
init_retry_after();
|
|
36962
37350
|
DEFAULT_RETRY = { count: 1, delayMs: 2e3 };
|
|
36963
37351
|
defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
37352
|
+
DEFAULT_REQUEST_TIMEOUT_MS = 3e4;
|
|
37353
|
+
DEFAULT_MAX_REDIRECTS = 5;
|
|
37354
|
+
REDIRECT_STATUSES = /* @__PURE__ */ new Set([301, 302, 303, 307, 308]);
|
|
36964
37355
|
UnauthorizedError = class extends Error {
|
|
36965
37356
|
status = 401;
|
|
37357
|
+
/** Always `'credential_rejected'` unless a subclass says otherwise. */
|
|
37358
|
+
kind = "credential_rejected";
|
|
36966
37359
|
constructor(service) {
|
|
36967
37360
|
super(`Unauthorized (401) from ${service} \u2014 the token is missing, invalid, or expired.`);
|
|
36968
37361
|
this.name = "UnauthorizedError";
|
|
@@ -36971,6 +37364,8 @@ var init_http = __esm({
|
|
|
36971
37364
|
};
|
|
36972
37365
|
RateLimitedError = class extends Error {
|
|
36973
37366
|
status = 429;
|
|
37367
|
+
/** `'http'`: the far side answered, with a 429 (see `status`). */
|
|
37368
|
+
kind = "http";
|
|
36974
37369
|
retryAfterMs;
|
|
36975
37370
|
constructor(service, retryAfterMs) {
|
|
36976
37371
|
super(`Rate limited (429) by ${service} after retries.`);
|
|
@@ -36981,6 +37376,8 @@ var init_http = __esm({
|
|
|
36981
37376
|
};
|
|
36982
37377
|
RequestTimeoutError = class extends Error {
|
|
36983
37378
|
timeoutMs;
|
|
37379
|
+
/** Always `'timeout'` unless a subclass says otherwise. */
|
|
37380
|
+
kind = "timeout";
|
|
36984
37381
|
constructor(service, timeoutMs) {
|
|
36985
37382
|
super(`Request to ${service} timed out after ${timeoutMs}ms.`);
|
|
36986
37383
|
this.name = "RequestTimeoutError";
|
|
@@ -36988,6 +37385,33 @@ var init_http = __esm({
|
|
|
36988
37385
|
Object.setPrototypeOf(this, new.target.prototype);
|
|
36989
37386
|
}
|
|
36990
37387
|
};
|
|
37388
|
+
DEFAULT_WRITE_OUTCOME_HINT = "The write may have happened \u2014 check before retrying; do not resend blindly.";
|
|
37389
|
+
WriteOutcomeUnknownError = class extends McpToolError {
|
|
37390
|
+
/** Always `true`: whether the write happened is not known. */
|
|
37391
|
+
outcomeUnknown = true;
|
|
37392
|
+
/** Always `false`: never replay this request without checking first. */
|
|
37393
|
+
retrySafe = false;
|
|
37394
|
+
/** `true` when the request's timeout expired; `false` for a dropped connection or broken body. */
|
|
37395
|
+
timedOut;
|
|
37396
|
+
/** The timeout that expired, when {@link timedOut}. */
|
|
37397
|
+
timeoutMs;
|
|
37398
|
+
/** The request method, upper-cased. */
|
|
37399
|
+
method;
|
|
37400
|
+
constructor(service, method, opts = {}) {
|
|
37401
|
+
const verb = method.toUpperCase();
|
|
37402
|
+
const what = opts.timeoutMs !== void 0 ? `${verb} to ${service} timed out after ${opts.timeoutMs}ms` : `${verb} to ${service} failed: ${truncateErrorMessage(messageOf(opts.cause), 200)}`;
|
|
37403
|
+
super(`${what} \u2014 the write may already have been applied (outcome is unknown).`, {
|
|
37404
|
+
hint: opts.hint ?? DEFAULT_WRITE_OUTCOME_HINT,
|
|
37405
|
+
cause: opts.cause,
|
|
37406
|
+
kind: opts.timeoutMs !== void 0 ? "timeout" : "transport"
|
|
37407
|
+
});
|
|
37408
|
+
this.name = "WriteOutcomeUnknownError";
|
|
37409
|
+
this.timedOut = opts.timeoutMs !== void 0;
|
|
37410
|
+
this.timeoutMs = opts.timeoutMs;
|
|
37411
|
+
this.method = verb;
|
|
37412
|
+
}
|
|
37413
|
+
};
|
|
37414
|
+
SAFE_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD", "OPTIONS", "TRACE"]);
|
|
36991
37415
|
ApiError = class extends Error {
|
|
36992
37416
|
status;
|
|
36993
37417
|
constructor(status, message) {
|
|
@@ -37000,6 +37424,8 @@ var init_http = __esm({
|
|
|
37000
37424
|
EdgeBlockedError = class extends ApiError {
|
|
37001
37425
|
/** Which edge refused the request, e.g. `'CloudFront'` or `'Cloudflare'`. */
|
|
37002
37426
|
vendor;
|
|
37427
|
+
/** Always `'edge_blocked'`: the credential was never judged. */
|
|
37428
|
+
kind = "edge_blocked";
|
|
37003
37429
|
/**
|
|
37004
37430
|
* @param where The service the request was for, and — when the thrower knows
|
|
37005
37431
|
* it — the method and path, which the message names.
|
|
@@ -37053,6 +37479,18 @@ var init_verify_after_write = __esm({
|
|
|
37053
37479
|
});
|
|
37054
37480
|
|
|
37055
37481
|
// node_modules/@chrischall/mcp-utils/dist/concurrency/index.js
|
|
37482
|
+
async function mapWithConcurrency(items, limit, fn) {
|
|
37483
|
+
const results = new Array(items.length);
|
|
37484
|
+
let next = 0;
|
|
37485
|
+
const poolSize = Math.min(Math.max(limit, 1), items.length);
|
|
37486
|
+
await Promise.all(Array.from({ length: poolSize }, async () => {
|
|
37487
|
+
while (next < items.length) {
|
|
37488
|
+
const i = next++;
|
|
37489
|
+
results[i] = await fn(items[i], i);
|
|
37490
|
+
}
|
|
37491
|
+
}));
|
|
37492
|
+
return results;
|
|
37493
|
+
}
|
|
37056
37494
|
var init_concurrency = __esm({
|
|
37057
37495
|
"node_modules/@chrischall/mcp-utils/dist/concurrency/index.js"() {
|
|
37058
37496
|
init_single_flight();
|
|
@@ -37063,6 +37501,7 @@ var init_concurrency = __esm({
|
|
|
37063
37501
|
// node_modules/@chrischall/mcp-utils/dist/dates/index.js
|
|
37064
37502
|
var init_dates = __esm({
|
|
37065
37503
|
"node_modules/@chrischall/mcp-utils/dist/dates/index.js"() {
|
|
37504
|
+
init_config();
|
|
37066
37505
|
}
|
|
37067
37506
|
});
|
|
37068
37507
|
|
|
@@ -37083,7 +37522,7 @@ var init_zod2 = __esm({
|
|
|
37083
37522
|
NonNegInt = external_exports.number().int().nonnegative();
|
|
37084
37523
|
NonEmptyString = external_exports.string().min(1);
|
|
37085
37524
|
IsoDate = external_exports.iso.date();
|
|
37086
|
-
IsoTime = external_exports.string().regex(/^([01]?\d|2[0-3]):[0-5]\d$/, "must be HH:MM (24h), e.g. 19:30");
|
|
37525
|
+
IsoTime = external_exports.string().regex(/^([01]?\d|2[0-3]):[0-5]\d$/, "must be HH:MM (24h), e.g. 19:30").overwrite((v) => v.padStart(5, "0"));
|
|
37087
37526
|
NumericIdString = external_exports.string().regex(/^\d+$/, "must be a numeric id (digits only)").describe("A numeric id string (digits only), safe to interpolate into a URL path.");
|
|
37088
37527
|
SafePathSegment = external_exports.string().min(1).regex(/^[^/?#\s]+$/, 'must not contain "/", "?", "#", or whitespace').refine((v) => !v.includes(".."), 'must not contain ".."').describe('A single URL path segment, safe to interpolate: no "/", "..", "?", "#", or whitespace.');
|
|
37089
37528
|
schemaOrigin = external_exports.string().optional().describe("Portal origin (e.g. https://<vendor>.example.co) selecting which active session to use. Optional when only one session is active.");
|
|
@@ -37118,6 +37557,7 @@ var init_auth = __esm({
|
|
|
37118
37557
|
init_config();
|
|
37119
37558
|
init_errors3();
|
|
37120
37559
|
init_http();
|
|
37560
|
+
init_cancel();
|
|
37121
37561
|
init_cached_token();
|
|
37122
37562
|
init_es256();
|
|
37123
37563
|
}
|
|
@@ -37148,7 +37588,7 @@ var init_version = __esm({
|
|
|
37148
37588
|
"src/version.ts"() {
|
|
37149
37589
|
"use strict";
|
|
37150
37590
|
PACKAGE_NAME = "office-outlook-mcp";
|
|
37151
|
-
VERSION = "0.
|
|
37591
|
+
VERSION = "0.5.0";
|
|
37152
37592
|
}
|
|
37153
37593
|
});
|
|
37154
37594
|
|
|
@@ -44875,7 +45315,7 @@ function assertUrlInDomains(field, url2, domains) {
|
|
|
44875
45315
|
const declared = domains.map((d) => JSON.stringify(d)).join(", ");
|
|
44876
45316
|
throw new Error(`FetchproxyServer: ${field} host "${host}" is outside declared domains [${declared}] \u2014 must be one of them or a subdomain`);
|
|
44877
45317
|
}
|
|
44878
|
-
function
|
|
45318
|
+
function parseJsonBody2(response) {
|
|
44879
45319
|
if (response.status === 204 || response.body.trim() === "")
|
|
44880
45320
|
return null;
|
|
44881
45321
|
return JSON.parse(response.body);
|
|
@@ -45944,7 +46384,7 @@ var init_ws_server = __esm({
|
|
|
45944
46384
|
*/
|
|
45945
46385
|
async getJson(path, opts = {}) {
|
|
45946
46386
|
const response = await this.get(path, this.applyJsonDefaults(opts));
|
|
45947
|
-
return
|
|
46387
|
+
return parseJsonBody2(response);
|
|
45948
46388
|
}
|
|
45949
46389
|
/**
|
|
45950
46390
|
* POST a JSON body and parse the response body as JSON. The body is
|
|
@@ -45963,7 +46403,7 @@ var init_ws_server = __esm({
|
|
|
45963
46403
|
headers,
|
|
45964
46404
|
body: body === void 0 ? void 0 : JSON.stringify(body)
|
|
45965
46405
|
});
|
|
45966
|
-
return
|
|
46406
|
+
return parseJsonBody2(response);
|
|
45967
46407
|
}
|
|
45968
46408
|
/**
|
|
45969
46409
|
* GET a path and return the response body as a string. Throws
|
|
@@ -46028,7 +46468,7 @@ var init_ws_server = __esm({
|
|
|
46028
46468
|
};
|
|
46029
46469
|
let data;
|
|
46030
46470
|
try {
|
|
46031
|
-
data =
|
|
46471
|
+
data = parseJsonBody2(response);
|
|
46032
46472
|
} catch (e) {
|
|
46033
46473
|
throw new Error(`fetchproxy ${method} ${path} \u2014 response was not JSON: ${e instanceof Error ? e.message : String(e)}`);
|
|
46034
46474
|
}
|
|
@@ -47164,15 +47604,12 @@ var init_dist4 = __esm({
|
|
|
47164
47604
|
});
|
|
47165
47605
|
|
|
47166
47606
|
// node_modules/@chrischall/mcp-utils/dist/healthcheck/index.js
|
|
47167
|
-
function statusOf2(err) {
|
|
47168
|
-
if (typeof err !== "object" || err === null)
|
|
47169
|
-
return void 0;
|
|
47170
|
-
const s = err.status ?? err.statusCode;
|
|
47171
|
-
return typeof s === "number" ? s : void 0;
|
|
47172
|
-
}
|
|
47173
47607
|
function isArm(kind) {
|
|
47174
47608
|
return kind !== void 0 && CREDENTIAL_ARMS.has(kind);
|
|
47175
47609
|
}
|
|
47610
|
+
function fromSource(source) {
|
|
47611
|
+
return source !== null ? ` from '${source}'` : "";
|
|
47612
|
+
}
|
|
47176
47613
|
function credentialHint(arm, prefix, hostLabel, source) {
|
|
47177
47614
|
switch (arm) {
|
|
47178
47615
|
case "ok":
|
|
@@ -47180,15 +47617,15 @@ function credentialHint(arm, prefix, hostLabel, source) {
|
|
|
47180
47617
|
case "no_credential":
|
|
47181
47618
|
return `No credential resolved. Nothing was available to authenticate with \u2014 sign in and reconnect the connector so ${prefix} receives a token, or set the documented environment variable.`;
|
|
47182
47619
|
case "credential_rejected":
|
|
47183
|
-
return `${hostLabel} rejected the credential
|
|
47620
|
+
return `${hostLabel} rejected the credential${fromSource(source)}. It is present but no longer valid \u2014 most often expired or revoked upstream. Re-authenticate and reconnect; retrying will not fix it.`;
|
|
47184
47621
|
case "edge_blocked":
|
|
47185
47622
|
return `${hostLabel} refused the request at its CDN/WAF before it reached the API, so the credential${source !== null ? ` from '${source}'` : ""} was never judged \u2014 re-signing in or rotating it will not help. This is usually a block on this host's IP address or request fingerprint: route through the ContextMint Bridge if this connector supports it, or retry later from a different network.`;
|
|
47186
47623
|
case "session_expired":
|
|
47187
|
-
return `The credential
|
|
47624
|
+
return `The credential${fromSource(source)} is configured, but no session is live \u2014 ${hostLabel} served a sign-in page rather than the data. Sign in again; a cookie-session portal expires these on its own, so this recurs between uses.`;
|
|
47188
47625
|
case "verification_pending":
|
|
47189
47626
|
return `${hostLabel} is holding the sign-in on a second factor rather than refusing it. Supply the verification code the ACCOUNT HOLDER received \u2014 the credential itself is not the problem, so changing it will not help.`;
|
|
47190
47627
|
case "timeout":
|
|
47191
|
-
return `The credential from '${source}' resolved, but ${hostLabel} did not answer in time. Usually transient \u2014 retry. If it persists, ${hostLabel} is slow or unreachable from here.`;
|
|
47628
|
+
return `${source !== null ? `The credential from '${source}' resolved, but ` : ""}${hostLabel} did not answer in time. Usually transient \u2014 retry. If it persists, ${hostLabel} is slow or unreachable from here.`;
|
|
47192
47629
|
case "http":
|
|
47193
47630
|
return `${hostLabel} answered with an error status that is not an auth rejection. That is USUALLY a ${hostLabel}-side problem rather than an auth one \u2014 but a 404 here more often means the probe path is wrong than that ${hostLabel} is broken, so check error.message and probe.url before concluding anything about the credential.`;
|
|
47194
47631
|
case "transport":
|
|
@@ -47209,7 +47646,8 @@ async function runCredentialHealthcheck(args) {
|
|
|
47209
47646
|
state = await resolveCredential();
|
|
47210
47647
|
} catch (e) {
|
|
47211
47648
|
const edge = edgeBlockOf(e);
|
|
47212
|
-
const
|
|
47649
|
+
const declared = errorKindOf(e);
|
|
47650
|
+
const classified = classifyThrown?.(e) ?? (edge !== null ? { kind: "edge_blocked", detail: { vendor: edge.vendor } } : declared !== void 0 ? { kind: declared } : void 0);
|
|
47213
47651
|
const result2 = {
|
|
47214
47652
|
ok: false,
|
|
47215
47653
|
// Still false, and still no source: a classification explains WHY
|
|
@@ -47256,10 +47694,11 @@ async function runCredentialHealthcheck(args) {
|
|
|
47256
47694
|
try {
|
|
47257
47695
|
await probeFn();
|
|
47258
47696
|
} catch (e) {
|
|
47259
|
-
status =
|
|
47697
|
+
status = errorStatusOf(e);
|
|
47260
47698
|
const aborted2 = e instanceof Error && e.name === "AbortError";
|
|
47261
47699
|
const edge = edgeBlockOf(e);
|
|
47262
|
-
|
|
47700
|
+
const declared = errorKindOf(e);
|
|
47701
|
+
arm = edge !== null ? "edge_blocked" : declared !== void 0 ? declared : status === 401 || status === 403 ? "credential_rejected" : status !== void 0 ? "http" : aborted2 || /timeout|timed out|ETIMEDOUT/i.test(messageOf(e)) ? "timeout" : /fetch failed|ENOTFOUND|ECONNREFUSED|ECONNRESET|network/i.test(messageOf(e)) ? "transport" : "unknown";
|
|
47263
47702
|
let kind = arm;
|
|
47264
47703
|
let detail = edge !== null ? { vendor: edge.vendor } : void 0;
|
|
47265
47704
|
const custom2 = classifyThrown?.(e);
|
|
@@ -47618,6 +48057,7 @@ init_dist2();
|
|
|
47618
48057
|
// node_modules/@chrischall/mcp-utils/dist/session/index.js
|
|
47619
48058
|
init_response();
|
|
47620
48059
|
init_config();
|
|
48060
|
+
init_errors3();
|
|
47621
48061
|
init_http();
|
|
47622
48062
|
init_edge_block();
|
|
47623
48063
|
import { existsSync as existsSync3, readFileSync as readFileSync3, mkdirSync as mkdirSync4, chmodSync as chmodSync2, renameSync as renameSync2, unlinkSync as unlinkSync4 } from "node:fs";
|
|
@@ -48099,7 +48539,9 @@ var TokenManager = class {
|
|
|
48099
48539
|
/** Spend `rt`, then install and persist what comes back. */
|
|
48100
48540
|
async exchange(rt) {
|
|
48101
48541
|
if (rt === void 0) {
|
|
48102
|
-
throw new
|
|
48542
|
+
throw new McpToolError("TokenManager: cannot refresh \u2014 no refresh token is available.", {
|
|
48543
|
+
kind: "no_credential"
|
|
48544
|
+
});
|
|
48103
48545
|
}
|
|
48104
48546
|
const tok = await this.refreshFn(rt);
|
|
48105
48547
|
this.tokens = {
|
|
@@ -48238,6 +48680,10 @@ function toBearerTokens(accessToken) {
|
|
|
48238
48680
|
}
|
|
48239
48681
|
return { accessToken, refreshToken: RECAPTURE_SENTINEL, expiresAt };
|
|
48240
48682
|
}
|
|
48683
|
+
function isCredentialFailure(e) {
|
|
48684
|
+
const kind = errorKindOf(e);
|
|
48685
|
+
return kind === "no_credential" || kind === "credential_rejected" || errorStatusOf(e) === 401;
|
|
48686
|
+
}
|
|
48241
48687
|
var OutlookClient = class {
|
|
48242
48688
|
#baseUrl;
|
|
48243
48689
|
/**
|
|
@@ -48265,7 +48711,8 @@ var OutlookClient = class {
|
|
|
48265
48711
|
this.#configError = new McpToolError(
|
|
48266
48712
|
"No Outlook credential is configured.",
|
|
48267
48713
|
{
|
|
48268
|
-
hint: "Set OUTLOOK_ACCESS_TOKEN, or unset OUTLOOK_DISABLE_FETCHPROXY so the token can be captured from a signed-in Outlook browser tab."
|
|
48714
|
+
hint: "Set OUTLOOK_ACCESS_TOKEN, or unset OUTLOOK_DISABLE_FETCHPROXY so the token can be captured from a signed-in Outlook browser tab.",
|
|
48715
|
+
kind: "no_credential"
|
|
48269
48716
|
}
|
|
48270
48717
|
);
|
|
48271
48718
|
}
|
|
@@ -48296,7 +48743,9 @@ var OutlookClient = class {
|
|
|
48296
48743
|
refresh: async () => {
|
|
48297
48744
|
if (direct !== void 0) {
|
|
48298
48745
|
throw new McpToolError("OUTLOOK_ACCESS_TOKEN was rejected by Outlook.", {
|
|
48299
|
-
hint: "A directly-supplied token cannot be refreshed automatically. Replace OUTLOOK_ACCESS_TOKEN with a current one, or unset it to capture from a signed-in Outlook browser tab instead."
|
|
48746
|
+
hint: "A directly-supplied token cannot be refreshed automatically. Replace OUTLOOK_ACCESS_TOKEN with a current one, or unset it to capture from a signed-in Outlook browser tab instead.",
|
|
48747
|
+
status: 401,
|
|
48748
|
+
kind: "credential_rejected"
|
|
48300
48749
|
});
|
|
48301
48750
|
}
|
|
48302
48751
|
return toBearerTokens(await this.#capture());
|
|
@@ -48335,7 +48784,9 @@ var OutlookClient = class {
|
|
|
48335
48784
|
timeout: 6e4,
|
|
48336
48785
|
baseHeaders: { Accept: "application/json" },
|
|
48337
48786
|
onUnauthorized: () => new McpToolError("Outlook rejected the access token (401).", {
|
|
48338
|
-
hint: "The token has expired or been revoked. Open a signed-in Outlook tab and retry \u2014 the next call re-captures automatically."
|
|
48787
|
+
hint: "The token has expired or been revoked. Open a signed-in Outlook tab and retry \u2014 the next call re-captures automatically.",
|
|
48788
|
+
status: 401,
|
|
48789
|
+
kind: "credential_rejected"
|
|
48339
48790
|
})
|
|
48340
48791
|
});
|
|
48341
48792
|
return this.#api;
|
|
@@ -48347,7 +48798,8 @@ var OutlookClient = class {
|
|
|
48347
48798
|
* — measured 9.4x smaller on a real message, and it also normalises a mixed
|
|
48348
48799
|
* HTML/Text collection to one type. `prefer` sets the `Prefer` header
|
|
48349
48800
|
* directly, for anything else the API takes there (notably
|
|
48350
|
-
* `outlook.timezone="…"`).
|
|
48801
|
+
* `outlook.timezone="…"`). `query` is serialised by mcp-utils'
|
|
48802
|
+
* `buildQueryString`, which drops `undefined`/empty values.
|
|
48351
48803
|
*/
|
|
48352
48804
|
async get(path, opts = {}) {
|
|
48353
48805
|
this.#requireConfigured();
|
|
@@ -48356,7 +48808,8 @@ var OutlookClient = class {
|
|
|
48356
48808
|
opts.prefer
|
|
48357
48809
|
].filter((p) => p !== void 0);
|
|
48358
48810
|
return this.#client().fetchJson("GET", path, {
|
|
48359
|
-
headers: prefer.length ? { Prefer: prefer.join(", ") } : void 0
|
|
48811
|
+
headers: prefer.length ? { Prefer: prefer.join(", ") } : void 0,
|
|
48812
|
+
...opts.query ? { query: opts.query } : {}
|
|
48360
48813
|
});
|
|
48361
48814
|
}
|
|
48362
48815
|
/**
|
|
@@ -48397,7 +48850,10 @@ var OutlookClient = class {
|
|
|
48397
48850
|
this.#requireConfigured();
|
|
48398
48851
|
return this.#client().fetchJson("POST", path, {
|
|
48399
48852
|
body,
|
|
48400
|
-
headers: opts.prefer ? { Prefer: opts.prefer } : void 0
|
|
48853
|
+
headers: opts.prefer ? { Prefer: opts.prefer } : void 0,
|
|
48854
|
+
// Read-only: a timeout or dropped connection is safe to retry, so keep
|
|
48855
|
+
// the plain error rather than mcp-utils' WriteOutcomeUnknownError.
|
|
48856
|
+
idempotent: true
|
|
48401
48857
|
});
|
|
48402
48858
|
}
|
|
48403
48859
|
/**
|
|
@@ -48442,6 +48898,7 @@ init_dist2();
|
|
|
48442
48898
|
|
|
48443
48899
|
// src/view.ts
|
|
48444
48900
|
init_dist2();
|
|
48901
|
+
var VIEWS2 = ["compact", "full", "raw"];
|
|
48445
48902
|
function addr(r) {
|
|
48446
48903
|
const e = r?.EmailAddress;
|
|
48447
48904
|
if (!e) return void 0;
|
|
@@ -48452,12 +48909,21 @@ function addrs(list) {
|
|
|
48452
48909
|
if (!Array.isArray(list) || list.length === 0) return void 0;
|
|
48453
48910
|
return list.map((r) => addr(r)).filter((s) => s !== void 0);
|
|
48454
48911
|
}
|
|
48912
|
+
var MAX_RECIPIENTS = 20;
|
|
48913
|
+
function cappedRecipients(key, list) {
|
|
48914
|
+
if (list === void 0 || list.length <= MAX_RECIPIENTS) return { [key]: list };
|
|
48915
|
+
return {
|
|
48916
|
+
[key]: list.slice(0, MAX_RECIPIENTS),
|
|
48917
|
+
[`${key}Count`]: list.length,
|
|
48918
|
+
[`${key}Truncated`]: true
|
|
48919
|
+
};
|
|
48920
|
+
}
|
|
48455
48921
|
function compactMessage(m) {
|
|
48456
48922
|
return pruned({
|
|
48457
48923
|
Id: m.Id,
|
|
48458
48924
|
Subject: m.Subject,
|
|
48459
48925
|
From: addr(m.From ?? m.Sender),
|
|
48460
|
-
To
|
|
48926
|
+
...cappedRecipients("To", addrs(m.ToRecipients)),
|
|
48461
48927
|
Received: m.ReceivedDateTime,
|
|
48462
48928
|
IsRead: m.IsRead,
|
|
48463
48929
|
HasAttachments: m.HasAttachments || void 0,
|
|
@@ -48470,7 +48936,7 @@ function compactMessage(m) {
|
|
|
48470
48936
|
function fullMessage(m) {
|
|
48471
48937
|
return pruned({
|
|
48472
48938
|
...compactMessage(m),
|
|
48473
|
-
Cc
|
|
48939
|
+
...cappedRecipients("Cc", addrs(m.CcRecipients)),
|
|
48474
48940
|
Sent: m.SentDateTime,
|
|
48475
48941
|
Importance: m.Importance,
|
|
48476
48942
|
IsDraft: m.IsDraft,
|
|
@@ -48489,14 +48955,19 @@ function compactEvent(e) {
|
|
|
48489
48955
|
TimeZone: e.Start?.TimeZone,
|
|
48490
48956
|
Location: e.Location?.DisplayName || void 0,
|
|
48491
48957
|
Organizer: addr(e.Organizer),
|
|
48492
|
-
IsAllDay: e.IsAllDay
|
|
48493
|
-
IsCancelled: e.IsCancelled
|
|
48958
|
+
IsAllDay: e.IsAllDay,
|
|
48959
|
+
IsCancelled: e.IsCancelled,
|
|
48960
|
+
IsOrganizer: e.IsOrganizer,
|
|
48961
|
+
MyResponse: e.ResponseStatus?.Response,
|
|
48962
|
+
ResponseRequested: e.ResponseRequested,
|
|
48963
|
+
Type: e.Type,
|
|
48964
|
+
ShowAs: e.ShowAs
|
|
48494
48965
|
});
|
|
48495
48966
|
}
|
|
48496
48967
|
function fullEvent(e) {
|
|
48497
48968
|
return pruned({
|
|
48498
48969
|
...compactEvent(e),
|
|
48499
|
-
|
|
48970
|
+
SeriesMasterId: e.SeriesMasterId,
|
|
48500
48971
|
Attendees: e.Attendees?.map(
|
|
48501
48972
|
(a) => pruned({ Who: addr(a), Response: a.Status?.Response })
|
|
48502
48973
|
),
|
|
@@ -48514,7 +48985,9 @@ function compactFolder(f) {
|
|
|
48514
48985
|
Id: f.Id,
|
|
48515
48986
|
Name: f.DisplayName,
|
|
48516
48987
|
Unread: f.UnreadItemCount,
|
|
48517
|
-
Total: f.TotalItemCount
|
|
48988
|
+
Total: f.TotalItemCount,
|
|
48989
|
+
// Listings are one level deep; a non-zero count says there is more below.
|
|
48990
|
+
Children: f.ChildFolderCount
|
|
48518
48991
|
});
|
|
48519
48992
|
}
|
|
48520
48993
|
function stripOData(o) {
|
|
@@ -48542,13 +49015,26 @@ function projectCollection(payload, project, label) {
|
|
|
48542
49015
|
});
|
|
48543
49016
|
}
|
|
48544
49017
|
|
|
49018
|
+
// src/tools/_invite.ts
|
|
49019
|
+
var EVENT_EXPAND = "Microsoft.OutlookServices.EventMessage/Event";
|
|
49020
|
+
function resolvedEvent(e) {
|
|
49021
|
+
return e?.Id ? e : void 0;
|
|
49022
|
+
}
|
|
49023
|
+
var INVITE_GONE = "the meeting is no longer on your calendar (already declined, cancelled, or deleted), so there is nothing to respond to";
|
|
49024
|
+
var MEETING_MESSAGE_TYPE = "Microsoft.OutlookServices.EventMessage/MeetingMessageType";
|
|
49025
|
+
function isMeetingMessage(m) {
|
|
49026
|
+
return !!m?.MeetingMessageType || (m?.["@odata.type"] ?? "").includes("EventMessage");
|
|
49027
|
+
}
|
|
49028
|
+
|
|
48545
49029
|
// src/tools/_paging.ts
|
|
48546
49030
|
init_zod();
|
|
48547
49031
|
var nextLinkParam = external_exports.string().min(1).optional().describe(
|
|
48548
49032
|
"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."
|
|
48549
49033
|
);
|
|
48550
49034
|
function fetchPage(client2, nextLink, path, opts = {}) {
|
|
48551
|
-
|
|
49035
|
+
if (nextLink === void 0) return client2.get(path, opts);
|
|
49036
|
+
const { query: _ignored, ...headerOpts } = opts;
|
|
49037
|
+
return client2.getAbsolute(nextLink, headerOpts);
|
|
48552
49038
|
}
|
|
48553
49039
|
function plainCollection(data) {
|
|
48554
49040
|
const items = Array.isArray(data?.value) ? data.value : [];
|
|
@@ -48567,7 +49053,6 @@ function mailboxUntrusted(payload) {
|
|
|
48567
49053
|
}
|
|
48568
49054
|
|
|
48569
49055
|
// src/tools/mail.ts
|
|
48570
|
-
var VIEWS2 = ["compact", "full", "raw"];
|
|
48571
49056
|
var WELL_KNOWN = [
|
|
48572
49057
|
"inbox",
|
|
48573
49058
|
"drafts",
|
|
@@ -48579,34 +49064,114 @@ var WELL_KNOWN = [
|
|
|
48579
49064
|
"clutter"
|
|
48580
49065
|
];
|
|
48581
49066
|
var LIST_SELECT = "Id,Subject,From,ToRecipients,ReceivedDateTime,IsRead,HasAttachments,BodyPreview";
|
|
48582
|
-
function qs(params) {
|
|
48583
|
-
const parts = [];
|
|
48584
|
-
for (const [k, v] of Object.entries(params)) {
|
|
48585
|
-
if (v === void 0 || v === "") continue;
|
|
48586
|
-
parts.push(`${k}=${encodeURIComponent(String(v))}`);
|
|
48587
|
-
}
|
|
48588
|
-
return parts.length ? `?${parts.join("&")}` : "";
|
|
48589
|
-
}
|
|
48590
49067
|
function folderSegment(folder) {
|
|
48591
49068
|
return encodeURIComponent(folder);
|
|
48592
49069
|
}
|
|
49070
|
+
var UNREAD_SELECT = [
|
|
49071
|
+
"Id",
|
|
49072
|
+
"Subject",
|
|
49073
|
+
"From",
|
|
49074
|
+
"ToRecipients",
|
|
49075
|
+
"CcRecipients",
|
|
49076
|
+
"ReceivedDateTime",
|
|
49077
|
+
"Importance",
|
|
49078
|
+
"HasAttachments",
|
|
49079
|
+
"Categories",
|
|
49080
|
+
"Flag",
|
|
49081
|
+
"ConversationId",
|
|
49082
|
+
MEETING_MESSAGE_TYPE
|
|
49083
|
+
].join(",");
|
|
49084
|
+
var BODY_CONCURRENCY = 4;
|
|
49085
|
+
function triageKind(m) {
|
|
49086
|
+
switch (m.MeetingMessageType) {
|
|
49087
|
+
case "MeetingRequest":
|
|
49088
|
+
return "meetingRequest";
|
|
49089
|
+
case "MeetingCancelled":
|
|
49090
|
+
return "meetingCancelled";
|
|
49091
|
+
case "MeetingAccepted":
|
|
49092
|
+
case "MeetingTenativelyAccepted":
|
|
49093
|
+
// sic — Outlook's spelling
|
|
49094
|
+
case "MeetingTentativelyAccepted":
|
|
49095
|
+
case "MeetingDeclined":
|
|
49096
|
+
return "meetingResponse";
|
|
49097
|
+
}
|
|
49098
|
+
const type = m["@odata.type"] ?? "";
|
|
49099
|
+
if (!type.includes("EventMessage")) return "mail";
|
|
49100
|
+
if (/Request$/.test(type)) return "meetingRequest";
|
|
49101
|
+
if (/Cancel/.test(type)) return "meetingCancelled";
|
|
49102
|
+
return "meetingResponse";
|
|
49103
|
+
}
|
|
49104
|
+
function triageEvent(e) {
|
|
49105
|
+
return stripUndefined({
|
|
49106
|
+
id: e.Id,
|
|
49107
|
+
subject: e.Subject,
|
|
49108
|
+
start: e.Start?.DateTime,
|
|
49109
|
+
end: e.End?.DateTime,
|
|
49110
|
+
timeZone: e.Start?.TimeZone,
|
|
49111
|
+
location: e.Location?.DisplayName || void 0,
|
|
49112
|
+
organizer: addr(e.Organizer),
|
|
49113
|
+
responseStatus: e.ResponseStatus?.Response,
|
|
49114
|
+
isCancelled: e.IsCancelled
|
|
49115
|
+
});
|
|
49116
|
+
}
|
|
49117
|
+
var INVITE_UNRESOLVED_HINT = `This invite's ${INVITE_GONE}.`;
|
|
49118
|
+
function truncateChars(text, max) {
|
|
49119
|
+
const end = /[\uD800-\uDBFF]/.test(text.charAt(max - 1)) ? max - 1 : max;
|
|
49120
|
+
return text.slice(0, end);
|
|
49121
|
+
}
|
|
49122
|
+
function stripUndefined(o) {
|
|
49123
|
+
return Object.fromEntries(Object.entries(o).filter(([, v]) => v !== void 0));
|
|
49124
|
+
}
|
|
49125
|
+
var ATTACHMENT_META_SELECT = "Id,Name,ContentType,Size,IsInline";
|
|
49126
|
+
var MAX_INLINE_BYTES = 5 * 1024 * 1024;
|
|
49127
|
+
var MAX_TEXT_BYTES = 200 * 1024;
|
|
49128
|
+
var TEXT_TYPE = /^text\/|^application\/(json|xml|csv|ics|calendar)\b|\+(json|xml)$/i;
|
|
49129
|
+
var TEXT_EXT = /\.(txt|csv|tsv|json|xml|ics|md|log|html?)$/i;
|
|
49130
|
+
function attachmentKind(a) {
|
|
49131
|
+
const type = a["@odata.type"] ?? "";
|
|
49132
|
+
if (/ItemAttachment$/.test(type)) return "item";
|
|
49133
|
+
if (/ReferenceAttachment$/.test(type)) return "reference";
|
|
49134
|
+
return "file";
|
|
49135
|
+
}
|
|
49136
|
+
function isImage(contentType) {
|
|
49137
|
+
return /^image\//i.test(contentType) && !/svg/i.test(contentType);
|
|
49138
|
+
}
|
|
49139
|
+
function isText(contentType, name) {
|
|
49140
|
+
return TEXT_TYPE.test(contentType) || /svg/i.test(contentType) || TEXT_EXT.test(name);
|
|
49141
|
+
}
|
|
49142
|
+
function base64DecodedLength(b64) {
|
|
49143
|
+
const padding = b64.endsWith("==") ? 2 : b64.endsWith("=") ? 1 : 0;
|
|
49144
|
+
return Math.floor(b64.length * 3 / 4) - padding;
|
|
49145
|
+
}
|
|
49146
|
+
function utf8Boundary(buf, max) {
|
|
49147
|
+
if (buf.length <= max) return buf.length;
|
|
49148
|
+
let end = max;
|
|
49149
|
+
while (end > 0 && (buf[end] & 192) === 128) end--;
|
|
49150
|
+
return end;
|
|
49151
|
+
}
|
|
49152
|
+
function sinceLiteral(hours) {
|
|
49153
|
+
return new Date(Date.now() - hours * 36e5).toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
49154
|
+
}
|
|
48593
49155
|
function registerMailTools(server, client2) {
|
|
48594
49156
|
server.registerTool(
|
|
48595
49157
|
"outlook_list_folders",
|
|
48596
49158
|
{
|
|
48597
|
-
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.",
|
|
48598
|
-
annotations: { readOnlyHint: true },
|
|
49159
|
+
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.",
|
|
49160
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
48599
49161
|
inputSchema: external_exports.object({
|
|
48600
49162
|
view: viewParam(VIEWS2),
|
|
49163
|
+
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."),
|
|
48601
49164
|
limit: external_exports.number().int().min(1).max(200).optional().describe("Max folders (default 50)"),
|
|
48602
49165
|
nextLink: nextLinkParam
|
|
48603
49166
|
})
|
|
48604
49167
|
},
|
|
48605
|
-
async ({ view, limit, nextLink }) => {
|
|
49168
|
+
async ({ view, parent, limit, nextLink }) => {
|
|
49169
|
+
const base = parent !== void 0 ? `/me/mailfolders/${folderSegment(parent)}/childfolders` : "/me/mailfolders";
|
|
48606
49170
|
const data = await fetchPage(
|
|
48607
49171
|
client2,
|
|
48608
49172
|
nextLink,
|
|
48609
|
-
|
|
49173
|
+
base,
|
|
49174
|
+
{ query: { $top: limit ?? 50 } }
|
|
48610
49175
|
);
|
|
48611
49176
|
if (resolveView(view, VIEWS2) === "raw") return minifiedResult(data);
|
|
48612
49177
|
return minifiedResult(projectCollection(data, compactFolder, "mail folder"));
|
|
@@ -48615,8 +49180,8 @@ function registerMailTools(server, client2) {
|
|
|
48615
49180
|
server.registerTool(
|
|
48616
49181
|
"outlook_list_messages",
|
|
48617
49182
|
{
|
|
48618
|
-
description:
|
|
48619
|
-
annotations: { readOnlyHint: true },
|
|
49183
|
+
description: 'List messages in a folder, newest first. Defaults to the inbox. Use `unreadOnly` for a triage view. Bodies are NOT included \u2014 call outlook_get_message for one. Note `search` and `unreadOnly` cannot be combined on a first-page request (the API rejects $search with $filter); pass `search` alone to search the whole mailbox. When following a `nextLink`, both are ignored, so echoing them back is harmless. Each message\'s To list (and Cc, in the full view) holds at most 20 addresses; a longer list adds ToCount/ToTruncated (CcCount/CcTruncated) with the full count \u2014 pass view:"raw" for every address. ' + UNTRUSTED_DESCRIPTION_SUFFIX,
|
|
49184
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
48620
49185
|
inputSchema: external_exports.object({
|
|
48621
49186
|
view: viewParam(VIEWS2),
|
|
48622
49187
|
folder: external_exports.string().optional().describe('Folder id or well-known name (default "inbox")'),
|
|
@@ -48648,7 +49213,8 @@ function registerMailTools(server, client2) {
|
|
|
48648
49213
|
const data = await fetchPage(
|
|
48649
49214
|
client2,
|
|
48650
49215
|
nextLink,
|
|
48651
|
-
|
|
49216
|
+
base,
|
|
49217
|
+
{ query: params }
|
|
48652
49218
|
);
|
|
48653
49219
|
const v = resolveView(view, VIEWS2);
|
|
48654
49220
|
if (v === "raw") return mailboxUntrusted(data);
|
|
@@ -48660,8 +49226,8 @@ function registerMailTools(server, client2) {
|
|
|
48660
49226
|
server.registerTool(
|
|
48661
49227
|
"outlook_get_message",
|
|
48662
49228
|
{
|
|
48663
|
-
description: `Get one message including its body. The body is requested as plain text rather than HTML (measured ~9x smaller and far easier to read); pass view:"raw" to get Outlook's untouched record. ` + UNTRUSTED_DESCRIPTION_SUFFIX,
|
|
48664
|
-
annotations: { readOnlyHint: true },
|
|
49229
|
+
description: `Get one message including its body. The body is requested as plain text rather than HTML (measured ~9x smaller and far easier to read); pass view:"raw" to get Outlook's untouched record. To and Cc list at most 20 addresses; a longer list adds ToCount/ToTruncated (CcCount/CcTruncated) with the full count \u2014 view:"raw" returns every address. ` + UNTRUSTED_DESCRIPTION_SUFFIX,
|
|
49230
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
48665
49231
|
inputSchema: external_exports.object({
|
|
48666
49232
|
view: viewParam(VIEWS2),
|
|
48667
49233
|
id: external_exports.string().min(1).describe("Message Id from outlook_list_messages"),
|
|
@@ -48677,11 +49243,96 @@ function registerMailTools(server, client2) {
|
|
|
48677
49243
|
return mailboxUntrusted(v === "full" ? fullMessage(data) : compactMessage(data));
|
|
48678
49244
|
}
|
|
48679
49245
|
);
|
|
49246
|
+
server.registerTool(
|
|
49247
|
+
"outlook_get_unread",
|
|
49248
|
+
{
|
|
49249
|
+
description: 'Triage batch: the unread messages in a folder (default inbox), newest first, each WITH its plain-text body (truncated to `maxBodyChars`) in one call \u2014 no follow-up outlook_get_message needed. Every item has a `kind`: "mail", "meetingRequest", "meetingCancelled" or "meetingResponse" (a reply to an invite you sent). Requests and cancellations also carry `event` (id, time, organizer, your current `responseStatus`) so the invite can be answered by its event id (when the meeting is no longer on your calendar \u2014 already declined, cancelled or deleted \u2014 the item has a `hint` instead and there is nothing to respond to). `to`/`cc` list at most 20 addresses; a longer list adds `toCount`/`ccCount` and `toTruncated`/`ccTruncated`. This does NOT mark anything read. Processing loop: read the batch \u2192 act on each item (reply, respond to the invite, file, flag \u2014 or leave it) \u2192 call outlook_mark_read on the ids you handled, so the next call returns only what is still new. Follow `nextLink` for more. ' + UNTRUSTED_DESCRIPTION_SUFFIX,
|
|
49250
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
49251
|
+
inputSchema: external_exports.object({
|
|
49252
|
+
folder: external_exports.string().min(1).optional().describe('Folder id or well-known name (default "inbox")'),
|
|
49253
|
+
limit: external_exports.number().int().min(1).max(50).optional().describe("Max messages (default 25, max 50)"),
|
|
49254
|
+
sinceHours: external_exports.number().positive().max(24 * 365).optional().describe("Only mail received in the last N hours"),
|
|
49255
|
+
includeBody: external_exports.boolean().optional().describe("Fetch each plain-text body (default true). Invite events are resolved either way."),
|
|
49256
|
+
maxBodyChars: external_exports.number().int().min(100).max(1e5).optional().describe("Truncate each body to this many characters (default 4000); `bodyTruncated` marks a cut"),
|
|
49257
|
+
nextLink: nextLinkParam
|
|
49258
|
+
})
|
|
49259
|
+
},
|
|
49260
|
+
async ({ folder, limit, sinceHours, includeBody, maxBodyChars, nextLink }) => {
|
|
49261
|
+
const filter = [
|
|
49262
|
+
sinceHours !== void 0 ? `ReceivedDateTime ge ${sinceLiteral(sinceHours)}` : void 0,
|
|
49263
|
+
"IsRead eq false"
|
|
49264
|
+
].filter((c) => c !== void 0).join(" and ");
|
|
49265
|
+
const data = await fetchPage(
|
|
49266
|
+
client2,
|
|
49267
|
+
nextLink,
|
|
49268
|
+
`/me/mailfolders/${folderSegment(folder ?? "inbox")}/messages`,
|
|
49269
|
+
{
|
|
49270
|
+
query: {
|
|
49271
|
+
$top: limit ?? 25,
|
|
49272
|
+
$select: UNREAD_SELECT,
|
|
49273
|
+
$filter: filter,
|
|
49274
|
+
$orderby: "ReceivedDateTime desc"
|
|
49275
|
+
}
|
|
49276
|
+
}
|
|
49277
|
+
);
|
|
49278
|
+
const rows = Array.isArray(data?.value) ? data.value : [];
|
|
49279
|
+
const wantBody = includeBody !== false;
|
|
49280
|
+
const cap = maxBodyChars ?? 4e3;
|
|
49281
|
+
const items = await mapWithConcurrency(rows, BODY_CONCURRENCY, async (m) => {
|
|
49282
|
+
const kind = triageKind(m);
|
|
49283
|
+
const item = stripUndefined({
|
|
49284
|
+
id: m.Id,
|
|
49285
|
+
kind,
|
|
49286
|
+
receivedAt: m.ReceivedDateTime,
|
|
49287
|
+
from: addr(m.From ?? m.Sender),
|
|
49288
|
+
...cappedRecipients("to", addrs(m.ToRecipients)),
|
|
49289
|
+
...cappedRecipients("cc", addrs(m.CcRecipients)),
|
|
49290
|
+
subject: m.Subject,
|
|
49291
|
+
importance: m.Importance,
|
|
49292
|
+
hasAttachments: m.HasAttachments || void 0,
|
|
49293
|
+
categories: m.Categories?.length ? m.Categories : void 0,
|
|
49294
|
+
flag: m.Flag?.FlagStatus,
|
|
49295
|
+
conversationId: m.ConversationId
|
|
49296
|
+
});
|
|
49297
|
+
const wantEvent = kind === "meetingRequest" || kind === "meetingCancelled";
|
|
49298
|
+
if (!wantBody && !wantEvent || m.Id === void 0) return item;
|
|
49299
|
+
try {
|
|
49300
|
+
const one = await client2.get(
|
|
49301
|
+
`/me/messages/${encodeURIComponent(m.Id)}`,
|
|
49302
|
+
{
|
|
49303
|
+
text: wantBody,
|
|
49304
|
+
query: {
|
|
49305
|
+
$select: wantBody ? "Id,Body" : "Id",
|
|
49306
|
+
$expand: wantEvent ? EVENT_EXPAND : void 0
|
|
49307
|
+
}
|
|
49308
|
+
}
|
|
49309
|
+
);
|
|
49310
|
+
if (wantBody) {
|
|
49311
|
+
const text = (one.Body?.Content ?? "").trim();
|
|
49312
|
+
item.body = text.length > cap ? truncateChars(text, cap) : text;
|
|
49313
|
+
if (text.length > cap) item.bodyTruncated = true;
|
|
49314
|
+
}
|
|
49315
|
+
if (wantEvent) {
|
|
49316
|
+
const event = resolvedEvent(one.Event);
|
|
49317
|
+
if (event) item.event = triageEvent(event);
|
|
49318
|
+
else item.hint = INVITE_UNRESOLVED_HINT;
|
|
49319
|
+
}
|
|
49320
|
+
} catch (e) {
|
|
49321
|
+
item.error = e instanceof Error ? e.message : String(e);
|
|
49322
|
+
}
|
|
49323
|
+
return item;
|
|
49324
|
+
});
|
|
49325
|
+
const out = { count: items.length, items };
|
|
49326
|
+
const next = data?.["@odata.nextLink"];
|
|
49327
|
+
if (next) out.nextLink = next;
|
|
49328
|
+
return mailboxUntrusted(out);
|
|
49329
|
+
}
|
|
49330
|
+
);
|
|
48680
49331
|
server.registerTool(
|
|
48681
49332
|
"outlook_list_attachments",
|
|
48682
49333
|
{
|
|
48683
49334
|
description: "List a message's attachments (name, size, content type). Metadata only \u2014 this deliberately does not download bytes.",
|
|
48684
|
-
annotations: { readOnlyHint: true },
|
|
49335
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
48685
49336
|
inputSchema: external_exports.object({
|
|
48686
49337
|
id: external_exports.string().min(1).describe("Message Id"),
|
|
48687
49338
|
nextLink: nextLinkParam
|
|
@@ -48691,13 +49342,76 @@ function registerMailTools(server, client2) {
|
|
|
48691
49342
|
const data = await fetchPage(
|
|
48692
49343
|
client2,
|
|
48693
49344
|
nextLink,
|
|
48694
|
-
`/me/messages/${encodeURIComponent(id)}/attachments
|
|
48695
|
-
|
|
48696
|
-
})}`
|
|
49345
|
+
`/me/messages/${encodeURIComponent(id)}/attachments`,
|
|
49346
|
+
{ query: { $select: "Id,Name,Size,ContentType" } }
|
|
48697
49347
|
);
|
|
48698
49348
|
return minifiedResult(plainCollection(data));
|
|
48699
49349
|
}
|
|
48700
49350
|
);
|
|
49351
|
+
server.registerTool(
|
|
49352
|
+
"outlook_get_attachment",
|
|
49353
|
+
{
|
|
49354
|
+
description: "Get one attachment's content (ids from outlook_list_attachments). An image comes back as image content the model can see; a text-like file (text/*, JSON, CSV, XML, .ics) is decoded to `text`, capped at 200 KB (`textTruncated` marks a cut). Any other binary (PDF, Office, archives) and anything over 5 MB is returned as metadata only \u2014 its bytes are not inlined. 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,
|
|
49355
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
49356
|
+
inputSchema: external_exports.object({
|
|
49357
|
+
messageId: external_exports.string().min(1).describe("Message Id"),
|
|
49358
|
+
attachmentId: external_exports.string().min(1).describe("Attachment Id from outlook_list_attachments")
|
|
49359
|
+
})
|
|
49360
|
+
},
|
|
49361
|
+
async ({ messageId, attachmentId }) => {
|
|
49362
|
+
const path = `/me/messages/${encodeURIComponent(messageId)}/attachments/${encodeURIComponent(attachmentId)}`;
|
|
49363
|
+
const meta3 = await client2.get(path, { query: { $select: ATTACHMENT_META_SELECT } });
|
|
49364
|
+
const kind = attachmentKind(meta3);
|
|
49365
|
+
const contentType = meta3.ContentType ?? "";
|
|
49366
|
+
const name = meta3.Name ?? "";
|
|
49367
|
+
const out = stripUndefined({
|
|
49368
|
+
messageId,
|
|
49369
|
+
attachmentId,
|
|
49370
|
+
name: meta3.Name,
|
|
49371
|
+
contentType: meta3.ContentType ?? void 0,
|
|
49372
|
+
size: meta3.Size,
|
|
49373
|
+
isInline: meta3.IsInline || void 0,
|
|
49374
|
+
kind,
|
|
49375
|
+
inlined: false
|
|
49376
|
+
});
|
|
49377
|
+
if (kind === "item") {
|
|
49378
|
+
out.hint = "This is an attached Outlook item (an email, event or contact), not a file, so it has no bytes to return. Ask the sender for it, or open the parent message in Outlook.";
|
|
49379
|
+
return mailboxUntrusted(out);
|
|
49380
|
+
}
|
|
49381
|
+
if (kind === "reference") {
|
|
49382
|
+
out.hint = "This is a link to a cloud file (OneDrive/SharePoint), not a file stored in the message, so there are no bytes to return.";
|
|
49383
|
+
return mailboxUntrusted(out);
|
|
49384
|
+
}
|
|
49385
|
+
const image = isImage(contentType);
|
|
49386
|
+
if (!image && !isText(contentType, name)) {
|
|
49387
|
+
out.hint = "Binary content is not inlined: only images and text-like files are returned. Open it in Outlook to read it.";
|
|
49388
|
+
return mailboxUntrusted(out);
|
|
49389
|
+
}
|
|
49390
|
+
if ((meta3.Size ?? 0) > MAX_INLINE_BYTES) {
|
|
49391
|
+
out.hint = "Content is not inlined above 5 MB. Open it in Outlook to read it.";
|
|
49392
|
+
return mailboxUntrusted(out);
|
|
49393
|
+
}
|
|
49394
|
+
const full = await client2.get(path);
|
|
49395
|
+
const bytes = full.ContentBytes;
|
|
49396
|
+
if (typeof bytes !== "string" || bytes.length === 0) {
|
|
49397
|
+
out.hint = "Outlook returned no content for this attachment.";
|
|
49398
|
+
return mailboxUntrusted(out);
|
|
49399
|
+
}
|
|
49400
|
+
if (base64DecodedLength(bytes) > MAX_INLINE_BYTES) {
|
|
49401
|
+
out.hint = "Content is not inlined above 5 MB. Open it in Outlook to read it.";
|
|
49402
|
+
return mailboxUntrusted(out);
|
|
49403
|
+
}
|
|
49404
|
+
const buf = Buffer.from(bytes, "base64");
|
|
49405
|
+
out.inlined = true;
|
|
49406
|
+
if (image) {
|
|
49407
|
+
const env = mailboxUntrusted(out);
|
|
49408
|
+
return { ...env, content: [...env.content, ...imageResult(bytes, contentType).content] };
|
|
49409
|
+
}
|
|
49410
|
+
out.text = buf.subarray(0, utf8Boundary(buf, MAX_TEXT_BYTES)).toString("utf8");
|
|
49411
|
+
if (buf.length > MAX_TEXT_BYTES) out.textTruncated = true;
|
|
49412
|
+
return mailboxUntrusted(out);
|
|
49413
|
+
}
|
|
49414
|
+
);
|
|
48701
49415
|
}
|
|
48702
49416
|
|
|
48703
49417
|
// src/tools/calendar.ts
|
|
@@ -48705,8 +49419,9 @@ init_zod();
|
|
|
48705
49419
|
init_dist2();
|
|
48706
49420
|
|
|
48707
49421
|
// src/timezone.ts
|
|
49422
|
+
init_dist2();
|
|
48708
49423
|
var zoneByClient = /* @__PURE__ */ new WeakMap();
|
|
48709
|
-
async function mailboxTimeZone(client2) {
|
|
49424
|
+
async function mailboxTimeZone(client2, opts = {}) {
|
|
48710
49425
|
const cached2 = zoneByClient.get(client2);
|
|
48711
49426
|
if (cached2) return cached2;
|
|
48712
49427
|
try {
|
|
@@ -48714,21 +49429,16 @@ async function mailboxTimeZone(client2) {
|
|
|
48714
49429
|
const zone = settings?.TimeZone?.trim() || void 0;
|
|
48715
49430
|
if (zone) zoneByClient.set(client2, zone);
|
|
48716
49431
|
return zone;
|
|
48717
|
-
} catch {
|
|
48718
|
-
return void 0;
|
|
49432
|
+
} catch (e) {
|
|
49433
|
+
if (!opts.required) return void 0;
|
|
49434
|
+
throw new McpToolError("Could not read the mailbox's time zone, so the event time is ambiguous.", {
|
|
49435
|
+
hint: `Retry, or pass \`timeZone\` explicitly (a Windows zone name such as "Eastern Standard Time"). Cause: ${e instanceof Error ? e.message : String(e)}`
|
|
49436
|
+
});
|
|
48719
49437
|
}
|
|
48720
49438
|
}
|
|
48721
49439
|
|
|
48722
49440
|
// src/tools/calendar.ts
|
|
48723
|
-
var EVENT_SELECT = "Id,Subject,Start,End,Location,Organizer,IsAllDay,IsCancelled,ShowAs,OnlineMeetingUrl,OnlineMeeting,BodyPreview";
|
|
48724
|
-
function qs2(params) {
|
|
48725
|
-
const parts = [];
|
|
48726
|
-
for (const [k, v] of Object.entries(params)) {
|
|
48727
|
-
if (v === void 0 || v === "") continue;
|
|
48728
|
-
parts.push(`${k}=${encodeURIComponent(String(v))}`);
|
|
48729
|
-
}
|
|
48730
|
-
return parts.length ? `?${parts.join("&")}` : "";
|
|
48731
|
-
}
|
|
49441
|
+
var EVENT_SELECT = "Id,Subject,Start,End,Location,Organizer,IsAllDay,IsCancelled,ShowAs,OnlineMeetingUrl,OnlineMeeting,BodyPreview,IsOrganizer,ResponseStatus,ResponseRequested,Type,SeriesMasterId";
|
|
48732
49442
|
function dateTimeTimeZone(value, zone) {
|
|
48733
49443
|
if (/(Z|[+-]\d{2}:?\d{2})$/i.test(value)) {
|
|
48734
49444
|
const d = new Date(value);
|
|
@@ -48756,7 +49466,7 @@ function registerCalendarTools(server, client2) {
|
|
|
48756
49466
|
"outlook_list_events",
|
|
48757
49467
|
{
|
|
48758
49468
|
description: 'List calendar events in a date window. This uses the calendar VIEW, which expands recurring series into their individual occurrences \u2014 the right tool for "what is on my schedule". Times are returned in `timeZone` when given. ' + UNTRUSTED_DESCRIPTION_SUFFIX,
|
|
48759
|
-
annotations: { readOnlyHint: true },
|
|
49469
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
48760
49470
|
inputSchema: external_exports.object({
|
|
48761
49471
|
view: viewParam(VIEWS2),
|
|
48762
49472
|
start: external_exports.string().min(1).describe("Window start, ISO 8601 (e.g. 2026-09-20T00:00:00Z)"),
|
|
@@ -48773,14 +49483,17 @@ function registerCalendarTools(server, client2) {
|
|
|
48773
49483
|
const data = await fetchPage(
|
|
48774
49484
|
client2,
|
|
48775
49485
|
nextLink,
|
|
48776
|
-
|
|
48777
|
-
|
|
48778
|
-
|
|
48779
|
-
|
|
48780
|
-
|
|
48781
|
-
|
|
48782
|
-
|
|
48783
|
-
|
|
49486
|
+
"/me/calendarview",
|
|
49487
|
+
{
|
|
49488
|
+
query: {
|
|
49489
|
+
startDateTime: start,
|
|
49490
|
+
endDateTime: end,
|
|
49491
|
+
$select: EVENT_SELECT,
|
|
49492
|
+
$orderby: "Start/DateTime",
|
|
49493
|
+
$top: limit ?? 50
|
|
49494
|
+
},
|
|
49495
|
+
...zone ? { prefer: `outlook.timezone="${zone}"` } : {}
|
|
49496
|
+
}
|
|
48784
49497
|
);
|
|
48785
49498
|
const v = resolveView(view, VIEWS2);
|
|
48786
49499
|
if (v === "raw") return mailboxUntrusted(data);
|
|
@@ -48793,7 +49506,7 @@ function registerCalendarTools(server, client2) {
|
|
|
48793
49506
|
"outlook_get_event",
|
|
48794
49507
|
{
|
|
48795
49508
|
description: "Get one calendar event in full, including attendees and their responses. " + UNTRUSTED_DESCRIPTION_SUFFIX,
|
|
48796
|
-
annotations: { readOnlyHint: true },
|
|
49509
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
48797
49510
|
inputSchema: external_exports.object({
|
|
48798
49511
|
view: viewParam(VIEWS2),
|
|
48799
49512
|
id: external_exports.string().min(1).describe("Event Id from outlook_list_events"),
|
|
@@ -48815,14 +49528,14 @@ function registerCalendarTools(server, client2) {
|
|
|
48815
49528
|
"outlook_list_calendars",
|
|
48816
49529
|
{
|
|
48817
49530
|
description: "List the calendars in the mailbox, for picking one to read or write against.",
|
|
48818
|
-
annotations: { readOnlyHint: true },
|
|
49531
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
48819
49532
|
inputSchema: external_exports.object({
|
|
48820
49533
|
view: viewParam(VIEWS2),
|
|
48821
49534
|
nextLink: nextLinkParam
|
|
48822
49535
|
})
|
|
48823
49536
|
},
|
|
48824
49537
|
async ({ view, nextLink }) => {
|
|
48825
|
-
const data = await fetchPage(client2, nextLink,
|
|
49538
|
+
const data = await fetchPage(client2, nextLink, "/me/calendars", { query: { $select: "Id,Name,Color,CanEdit,Owner" } });
|
|
48826
49539
|
if (resolveView(view, VIEWS2) === "raw") return minifiedResult(data);
|
|
48827
49540
|
return minifiedResult(plainCollection(data));
|
|
48828
49541
|
}
|
|
@@ -48830,12 +49543,13 @@ function registerCalendarTools(server, client2) {
|
|
|
48830
49543
|
server.registerTool(
|
|
48831
49544
|
"outlook_find_meeting_times",
|
|
48832
49545
|
{
|
|
48833
|
-
description: "Find times when people can meet \u2014 Outlook's Scheduling Assistant. Give the attendees, a search window and a duration; Outlook checks everyone's free/busy (and the signed-in user's, as organizer) and returns ranked candidate slots with each attendee's availability. By default only working hours are searched. Use this to answer \"when can X and Y meet?\"; then book with outlook_create_event. `start`/`end` are local wall-clock times in `timeZone` (default: the mailbox zone), and returned slots are in that zone too. " + UNTRUSTED_DESCRIPTION_SUFFIX,
|
|
48834
|
-
annotations: { readOnlyHint: true },
|
|
49546
|
+
description: "Find times when people can meet \u2014 Outlook's Scheduling Assistant. Give the attendees, a search window and a duration; Outlook checks everyone's free/busy (and the signed-in user's, as organizer) and returns ranked candidate slots with each attendee's availability. By default only working hours are searched. Use this to answer \"when can X and Y meet?\"; then book with outlook_create_event \u2014 each suggestion carries `createEventArgs` (times, zone and attendees; `subject` too when given here) to pass to it as-is. `start`/`end` are local wall-clock times in `timeZone` (default: the mailbox zone), and returned slots are in that zone too. view: 'raw' returns Outlook's response as-is, without `createEventArgs`. " + UNTRUSTED_DESCRIPTION_SUFFIX,
|
|
49547
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
48835
49548
|
inputSchema: external_exports.object({
|
|
48836
49549
|
view: viewParam(VIEWS2),
|
|
48837
49550
|
attendees: attendeeList("required attendees").min(1),
|
|
48838
49551
|
optionalAttendees: attendeeList("optional attendees").optional(),
|
|
49552
|
+
subject: external_exports.string().optional().describe("Meeting title, carried into each suggestion's createEventArgs; not sent to Outlook"),
|
|
48839
49553
|
start: external_exports.string().min(1).describe("Earliest the meeting may start, e.g. 2026-10-12T09:00:00"),
|
|
48840
49554
|
end: external_exports.string().min(1).describe("Latest the meeting may end, e.g. 2026-10-16T17:00:00"),
|
|
48841
49555
|
durationMinutes: external_exports.number().int().min(5).max(1440).describe("Meeting length in minutes"),
|
|
@@ -48871,19 +49585,37 @@ function registerCalendarTools(server, client2) {
|
|
|
48871
49585
|
};
|
|
48872
49586
|
const data = await client2.post("/me/findmeetingtimes", body, { prefer: `outlook.timezone="${zone}"` });
|
|
48873
49587
|
if (resolveView(args.view, VIEWS2) === "raw") return mailboxUntrusted(data);
|
|
48874
|
-
const suggestions = (data?.MeetingTimeSuggestions ?? []).map((s) =>
|
|
48875
|
-
|
|
48876
|
-
|
|
48877
|
-
|
|
48878
|
-
|
|
48879
|
-
|
|
48880
|
-
|
|
48881
|
-
|
|
48882
|
-
|
|
48883
|
-
|
|
48884
|
-
|
|
48885
|
-
|
|
48886
|
-
|
|
49588
|
+
const suggestions = (data?.MeetingTimeSuggestions ?? []).map((s) => {
|
|
49589
|
+
const start = trimTime(s.MeetingTimeSlot?.Start?.DateTime);
|
|
49590
|
+
const end = trimTime(s.MeetingTimeSlot?.End?.DateTime);
|
|
49591
|
+
return {
|
|
49592
|
+
Start: start,
|
|
49593
|
+
End: end,
|
|
49594
|
+
Confidence: s.Confidence,
|
|
49595
|
+
Organizer: s.OrganizerAvailability,
|
|
49596
|
+
Attendees: Object.fromEntries(
|
|
49597
|
+
(s.AttendeeAvailability ?? []).map((a) => [
|
|
49598
|
+
a.Attendee?.EmailAddress?.Address ?? a.Attendee?.EmailAddress?.Name ?? "?",
|
|
49599
|
+
a.Availability
|
|
49600
|
+
])
|
|
49601
|
+
),
|
|
49602
|
+
Reason: s.SuggestionReason,
|
|
49603
|
+
// outlook_create_event's input for this slot, so booking it is a
|
|
49604
|
+
// straight hand-off. The slot came back in `zone` (the Prefer header),
|
|
49605
|
+
// which is also the zone create reads wall-clock times in. A slot
|
|
49606
|
+
// without times cannot be booked, so it gets no args.
|
|
49607
|
+
...start && end ? {
|
|
49608
|
+
createEventArgs: {
|
|
49609
|
+
...args.subject !== void 0 ? { subject: args.subject } : {},
|
|
49610
|
+
start,
|
|
49611
|
+
end,
|
|
49612
|
+
timeZone: zone,
|
|
49613
|
+
attendees: args.attendees,
|
|
49614
|
+
...args.optionalAttendees?.length ? { optionalAttendees: args.optionalAttendees } : {}
|
|
49615
|
+
}
|
|
49616
|
+
} : {}
|
|
49617
|
+
};
|
|
49618
|
+
});
|
|
48887
49619
|
return mailboxUntrusted({
|
|
48888
49620
|
timeZone: zone,
|
|
48889
49621
|
count: suggestions.length,
|
|
@@ -48898,7 +49630,7 @@ function registerCalendarTools(server, client2) {
|
|
|
48898
49630
|
"outlook_get_schedule",
|
|
48899
49631
|
{
|
|
48900
49632
|
description: "Get people's free/busy blocks for a time window \u2014 the grid view of Outlook's Scheduling Assistant. Returns each person's busy, tentative and out-of-office items (with subject and location where their calendar shares them). Use outlook_find_meeting_times to have Outlook pick slots instead. `start`/`end` are local wall-clock times in `timeZone` (default: the mailbox zone). " + UNTRUSTED_DESCRIPTION_SUFFIX,
|
|
48901
|
-
annotations: { readOnlyHint: true },
|
|
49633
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
48902
49634
|
inputSchema: external_exports.object({
|
|
48903
49635
|
view: viewParam(VIEWS2),
|
|
48904
49636
|
people: attendeeList("people (or rooms) to look up").min(1).max(100),
|
|
@@ -48945,14 +49677,6 @@ function registerCalendarTools(server, client2) {
|
|
|
48945
49677
|
// src/tools/directory.ts
|
|
48946
49678
|
init_zod();
|
|
48947
49679
|
init_dist2();
|
|
48948
|
-
function qs3(params) {
|
|
48949
|
-
const parts = [];
|
|
48950
|
-
for (const [k, v] of Object.entries(params)) {
|
|
48951
|
-
if (v === void 0 || v === "") continue;
|
|
48952
|
-
parts.push(`${k}=${encodeURIComponent(String(v))}`);
|
|
48953
|
-
}
|
|
48954
|
-
return parts.length ? `?${parts.join("&")}` : "";
|
|
48955
|
-
}
|
|
48956
49680
|
function collection(data, raw) {
|
|
48957
49681
|
if (raw) return data;
|
|
48958
49682
|
return plainCollection(data);
|
|
@@ -48962,7 +49686,7 @@ function registerDirectoryTools(server, client2) {
|
|
|
48962
49686
|
"outlook_get_profile",
|
|
48963
49687
|
{
|
|
48964
49688
|
description: "Get the signed-in mailbox's identity (email address, display name, alias). The cheapest call that proves the credential works.",
|
|
48965
|
-
annotations: { readOnlyHint: true },
|
|
49689
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
48966
49690
|
inputSchema: external_exports.object({})
|
|
48967
49691
|
},
|
|
48968
49692
|
async () => minifiedResult(stripOData(await client2.get("/me")))
|
|
@@ -48971,7 +49695,7 @@ function registerDirectoryTools(server, client2) {
|
|
|
48971
49695
|
"outlook_get_mailbox_settings",
|
|
48972
49696
|
{
|
|
48973
49697
|
description: "Get mailbox settings: time zone, working hours, language, and automatic-replies (out-of-office) configuration.",
|
|
48974
|
-
annotations: { readOnlyHint: true },
|
|
49698
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
48975
49699
|
inputSchema: external_exports.object({})
|
|
48976
49700
|
},
|
|
48977
49701
|
async () => minifiedResult(stripOData(await client2.get("/me/MailboxSettings")))
|
|
@@ -48980,7 +49704,7 @@ function registerDirectoryTools(server, client2) {
|
|
|
48980
49704
|
"outlook_list_contacts",
|
|
48981
49705
|
{
|
|
48982
49706
|
description: "List saved contacts from the mailbox address book.",
|
|
48983
|
-
annotations: { readOnlyHint: true },
|
|
49707
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
48984
49708
|
inputSchema: external_exports.object({
|
|
48985
49709
|
view: viewParam(VIEWS2),
|
|
48986
49710
|
limit: external_exports.number().int().min(1).max(200).optional().describe("Max contacts (default 50)"),
|
|
@@ -48992,13 +49716,16 @@ function registerDirectoryTools(server, client2) {
|
|
|
48992
49716
|
const data = await fetchPage(
|
|
48993
49717
|
client2,
|
|
48994
49718
|
nextLink,
|
|
48995
|
-
|
|
48996
|
-
|
|
48997
|
-
|
|
48998
|
-
|
|
48999
|
-
|
|
49000
|
-
|
|
49001
|
-
|
|
49719
|
+
"/me/contacts",
|
|
49720
|
+
{
|
|
49721
|
+
query: {
|
|
49722
|
+
$top: limit ?? 50,
|
|
49723
|
+
$skip: skip,
|
|
49724
|
+
// `MobilePhone1`, not `MobilePhone`: the latter is the Graph name and
|
|
49725
|
+
// the v2.0 Contact type rejects it outright with a 400.
|
|
49726
|
+
$select: "Id,DisplayName,EmailAddresses,CompanyName,JobTitle,MobilePhone1"
|
|
49727
|
+
}
|
|
49728
|
+
}
|
|
49002
49729
|
);
|
|
49003
49730
|
return minifiedResult(collection(data, resolveView(view, VIEWS2) === "raw"));
|
|
49004
49731
|
}
|
|
@@ -49007,7 +49734,7 @@ function registerDirectoryTools(server, client2) {
|
|
|
49007
49734
|
"outlook_list_people",
|
|
49008
49735
|
{
|
|
49009
49736
|
description: 'List people ranked by relevance to the user \u2014 colleagues and frequent correspondents, drawn from mail traffic rather than the saved address book. Better than outlook_list_contacts for resolving "who is X" in a work mailbox.',
|
|
49010
|
-
annotations: { readOnlyHint: true },
|
|
49737
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
49011
49738
|
inputSchema: external_exports.object({
|
|
49012
49739
|
view: viewParam(VIEWS2),
|
|
49013
49740
|
limit: external_exports.number().int().min(1).max(100).optional().describe("Max people (default 25)"),
|
|
@@ -49018,10 +49745,13 @@ function registerDirectoryTools(server, client2) {
|
|
|
49018
49745
|
const data = await fetchPage(
|
|
49019
49746
|
client2,
|
|
49020
49747
|
nextLink,
|
|
49021
|
-
|
|
49022
|
-
|
|
49023
|
-
|
|
49024
|
-
|
|
49748
|
+
"/me/people",
|
|
49749
|
+
{
|
|
49750
|
+
query: {
|
|
49751
|
+
$top: limit ?? 25,
|
|
49752
|
+
$select: "Id,DisplayName,ScoredEmailAddresses,JobTitle,CompanyName"
|
|
49753
|
+
}
|
|
49754
|
+
}
|
|
49025
49755
|
);
|
|
49026
49756
|
return minifiedResult(collection(data, resolveView(view, VIEWS2) === "raw"));
|
|
49027
49757
|
}
|
|
@@ -49030,7 +49760,7 @@ function registerDirectoryTools(server, client2) {
|
|
|
49030
49760
|
"outlook_list_tasks",
|
|
49031
49761
|
{
|
|
49032
49762
|
description: "List Outlook tasks with status and due dates.",
|
|
49033
|
-
annotations: { readOnlyHint: true },
|
|
49763
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
49034
49764
|
inputSchema: external_exports.object({
|
|
49035
49765
|
view: viewParam(VIEWS2),
|
|
49036
49766
|
limit: external_exports.number().int().min(1).max(200).optional().describe("Max tasks (default 50)"),
|
|
@@ -49041,14 +49771,32 @@ function registerDirectoryTools(server, client2) {
|
|
|
49041
49771
|
const data = await fetchPage(
|
|
49042
49772
|
client2,
|
|
49043
49773
|
nextLink,
|
|
49044
|
-
|
|
49045
|
-
|
|
49046
|
-
|
|
49047
|
-
|
|
49774
|
+
"/me/tasks",
|
|
49775
|
+
{
|
|
49776
|
+
query: {
|
|
49777
|
+
$top: limit ?? 50,
|
|
49778
|
+
$select: "Id,Subject,Status,Importance,DueDateTime,CompletedDateTime"
|
|
49779
|
+
}
|
|
49780
|
+
}
|
|
49048
49781
|
);
|
|
49049
49782
|
return minifiedResult(collection(data, resolveView(view, VIEWS2) === "raw"));
|
|
49050
49783
|
}
|
|
49051
49784
|
);
|
|
49785
|
+
server.registerTool(
|
|
49786
|
+
"outlook_list_categories",
|
|
49787
|
+
{
|
|
49788
|
+
description: "List the mailbox's categories (the master list) with their colours \u2014 the names outlook_update_message can add or remove.",
|
|
49789
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
49790
|
+
inputSchema: external_exports.object({})
|
|
49791
|
+
},
|
|
49792
|
+
async () => {
|
|
49793
|
+
const data = await client2.get(
|
|
49794
|
+
"/me/outlook/masterCategories"
|
|
49795
|
+
);
|
|
49796
|
+
const categories = (data?.value ?? []).map((c) => ({ name: c.DisplayName, color: c.Color }));
|
|
49797
|
+
return minifiedResult({ count: categories.length, categories });
|
|
49798
|
+
}
|
|
49799
|
+
);
|
|
49052
49800
|
}
|
|
49053
49801
|
|
|
49054
49802
|
// src/tools/writes.ts
|
|
@@ -49086,15 +49834,93 @@ function dateTimeTimeZone2(value, zone) {
|
|
|
49086
49834
|
}
|
|
49087
49835
|
return { DateTime: value, TimeZone: zone };
|
|
49088
49836
|
}
|
|
49837
|
+
var TIMEOUT_SENTENCE = 'If Outlook does not answer in time (or the connection drops) the result has status "unknown": the write may well have gone through, so check before retrying \u2014 never resend blindly.';
|
|
49838
|
+
async function writeOrUnknown(run, checkWhere) {
|
|
49839
|
+
try {
|
|
49840
|
+
return { ok: true, value: await run() };
|
|
49841
|
+
} catch (e) {
|
|
49842
|
+
if (!(e instanceof WriteOutcomeUnknownError)) throw e;
|
|
49843
|
+
return {
|
|
49844
|
+
ok: false,
|
|
49845
|
+
result: minifiedResult({
|
|
49846
|
+
status: "unknown",
|
|
49847
|
+
warning: `Outlook did not confirm this write (${e.timedOut ? "it did not answer in time" : "the connection dropped"}), so it may already have happened. Check ${checkWhere} before retrying; do not resend blindly.`
|
|
49848
|
+
})
|
|
49849
|
+
};
|
|
49850
|
+
}
|
|
49851
|
+
}
|
|
49852
|
+
var INVITE_EVENT_SELECT = "Id,Subject,IsOrganizer,IsCancelled,Start,End,Organizer,ResponseStatus";
|
|
49853
|
+
var DELETE_EVENT_SELECT = "Id,Subject,IsOrganizer,IsCancelled,ResponseStatus,Type,SeriesMasterId,Start,End,Organizer,Attendees";
|
|
49854
|
+
var INVITE_RESPONSES = {
|
|
49855
|
+
accept: { verb: "accept", status: "Accepted" },
|
|
49856
|
+
tentative: { verb: "tentativelyaccept", status: "TentativelyAccepted" },
|
|
49857
|
+
decline: { verb: "decline", status: "Declined" }
|
|
49858
|
+
};
|
|
49089
49859
|
var naiveMs = (dt) => Date.parse(`${dt.replace(/\.\d+$/, "")}Z`);
|
|
49090
49860
|
var fromNaiveMs = (ms) => new Date(ms).toISOString().replace(/\.\d{3}Z$/, "");
|
|
49091
49861
|
var trimTime2 = (dt) => dt?.replace(/\.\d+$/, "");
|
|
49862
|
+
var REPLY_MODES = {
|
|
49863
|
+
reply: { send: "reply", draft: "createreply", prefix: "RE: " },
|
|
49864
|
+
replyAll: { send: "replyall", draft: "createreplyall", prefix: "RE: " },
|
|
49865
|
+
forward: { send: "forward", draft: "createforward", prefix: "FW: " }
|
|
49866
|
+
};
|
|
49867
|
+
function addresses(...lists) {
|
|
49868
|
+
const seen = /* @__PURE__ */ new Map();
|
|
49869
|
+
for (const a of lists.flat()) {
|
|
49870
|
+
const v = a?.EmailAddress?.Address;
|
|
49871
|
+
if (v && !seen.has(v.toLowerCase())) seen.set(v.toLowerCase(), v);
|
|
49872
|
+
}
|
|
49873
|
+
return [...seen.values()];
|
|
49874
|
+
}
|
|
49875
|
+
var escapeHtml = (t) => t.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
49876
|
+
function bodyWithComment(body, comment) {
|
|
49877
|
+
const existing = body?.Content ?? "";
|
|
49878
|
+
if (body?.ContentType?.toLowerCase() !== "html") {
|
|
49879
|
+
return { ContentType: "Text", Content: existing ? `${comment}
|
|
49880
|
+
|
|
49881
|
+
${existing}` : comment };
|
|
49882
|
+
}
|
|
49883
|
+
const block = `<div>${escapeHtml(comment).replace(/\r?\n/g, "<br>")}</div><br>`;
|
|
49884
|
+
const open2 = /<body[^>]*>/i.exec(existing);
|
|
49885
|
+
const Content = open2 ? existing.slice(0, open2.index + open2[0].length) + block + existing.slice(open2.index + open2[0].length) : block + existing;
|
|
49886
|
+
return { ContentType: "HTML", Content };
|
|
49887
|
+
}
|
|
49888
|
+
var FLAG_STATUS = { flagged: "Flagged", complete: "Complete", none: "NotFlagged" };
|
|
49889
|
+
var UPDATE_CONCURRENCY = 4;
|
|
49890
|
+
var sameCategories = (a, b) => [...a ?? []].map((c) => c.toLowerCase()).sort().join("\n") === [...b ?? []].map((c) => c.toLowerCase()).sort().join("\n");
|
|
49891
|
+
function editCategories(current, add, remove) {
|
|
49892
|
+
const gone = new Set(remove.map((c) => c.toLowerCase()));
|
|
49893
|
+
const next = (current ?? []).filter((c) => !gone.has(c.toLowerCase()));
|
|
49894
|
+
for (const c of add) {
|
|
49895
|
+
if (!next.some((n) => n.toLowerCase() === c.toLowerCase())) next.push(c);
|
|
49896
|
+
}
|
|
49897
|
+
return next;
|
|
49898
|
+
}
|
|
49899
|
+
function unchangedFields(payload, after) {
|
|
49900
|
+
const out = [];
|
|
49901
|
+
if (payload.IsRead !== void 0 && after.IsRead !== payload.IsRead) out.push("IsRead");
|
|
49902
|
+
if (payload.Flag && after.Flag?.FlagStatus !== payload.Flag.FlagStatus) out.push("Flag");
|
|
49903
|
+
if (payload.Categories && !sameCategories(after.Categories, payload.Categories)) out.push("Categories");
|
|
49904
|
+
return out;
|
|
49905
|
+
}
|
|
49906
|
+
var createEventInput = external_exports.object({
|
|
49907
|
+
subject: external_exports.string().describe("Event title"),
|
|
49908
|
+
start: external_exports.string().min(1).describe("Start, ISO 8601 local time e.g. 2026-09-22T15:00:00"),
|
|
49909
|
+
end: external_exports.string().min(1).describe("End, ISO 8601 local time"),
|
|
49910
|
+
timeZone: external_exports.string().optional().describe("Windows time-zone name. Defaults to the MAILBOX time zone."),
|
|
49911
|
+
location: external_exports.string().optional().describe("Location display name"),
|
|
49912
|
+
body: external_exports.string().optional().describe("Event description"),
|
|
49913
|
+
attendees: recipientList.describe("Required attendees' email addresses"),
|
|
49914
|
+
optionalAttendees: recipientList.describe("Optional attendees' email addresses"),
|
|
49915
|
+
teamsMeeting: teamsMeetingParam,
|
|
49916
|
+
confirmToken: confirmTokenParam
|
|
49917
|
+
});
|
|
49092
49918
|
function registerWriteTools(server, client2) {
|
|
49093
49919
|
server.registerTool(
|
|
49094
49920
|
"outlook_send_mail",
|
|
49095
49921
|
{
|
|
49096
|
-
description: "Send an email from the signed-in mailbox. " + CONFIRM_FLOW_SENTENCE + " The preview shows exactly what would be sent. " + CONFIRM_INJECTION_RULE,
|
|
49097
|
-
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
49922
|
+
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,
|
|
49923
|
+
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
|
|
49098
49924
|
inputSchema: external_exports.object({
|
|
49099
49925
|
to: recipientList,
|
|
49100
49926
|
cc: recipientList,
|
|
@@ -49128,7 +49954,8 @@ function registerWriteTools(server, client2) {
|
|
|
49128
49954
|
confirmToken
|
|
49129
49955
|
});
|
|
49130
49956
|
if (gate) return gate;
|
|
49131
|
-
await client2.write("POST", "/me/sendmail", payload);
|
|
49957
|
+
const sent = await writeOrUnknown(() => client2.write("POST", "/me/sendmail", payload), "Sent Items");
|
|
49958
|
+
if (!sent.ok) return sent.result;
|
|
49132
49959
|
return minifiedResult({ sent: true, subject, recipients: recipients || null });
|
|
49133
49960
|
}
|
|
49134
49961
|
);
|
|
@@ -49136,7 +49963,7 @@ function registerWriteTools(server, client2) {
|
|
|
49136
49963
|
"outlook_create_draft",
|
|
49137
49964
|
{
|
|
49138
49965
|
description: "Create a draft message in the Drafts folder without sending it. Returns the created draft, which can be reviewed and sent from Outlook. " + CONFIRM_FLOW_SENTENCE,
|
|
49139
|
-
annotations: { readOnlyHint: false, destructiveHint:
|
|
49966
|
+
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
|
|
49140
49967
|
inputSchema: external_exports.object({
|
|
49141
49968
|
to: recipientList,
|
|
49142
49969
|
cc: recipientList,
|
|
@@ -49175,7 +50002,7 @@ function registerWriteTools(server, client2) {
|
|
|
49175
50002
|
"outlook_mark_read",
|
|
49176
50003
|
{
|
|
49177
50004
|
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,
|
|
49178
|
-
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true },
|
|
50005
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
49179
50006
|
inputSchema: external_exports.object({
|
|
49180
50007
|
id: external_exports.string().min(1).describe("Message Id"),
|
|
49181
50008
|
isRead: external_exports.boolean().describe("true to mark read, false to mark unread"),
|
|
@@ -49206,11 +50033,90 @@ function registerWriteTools(server, client2) {
|
|
|
49206
50033
|
});
|
|
49207
50034
|
}
|
|
49208
50035
|
);
|
|
50036
|
+
server.registerTool(
|
|
50037
|
+
"outlook_update_message",
|
|
50038
|
+
{
|
|
50039
|
+
description: 'Flag, categorise and/or mark read up to 50 messages at once \u2014 the filing step of the triage loop (outlook_get_unread \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,
|
|
50040
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
50041
|
+
inputSchema: external_exports.object({
|
|
50042
|
+
messageIds: external_exports.array(external_exports.string().min(1)).min(1).max(50).describe("Message Ids (1 to 50)"),
|
|
50043
|
+
flag: external_exports.enum(["flagged", "complete", "none"]).optional().describe("Follow-up flag to set"),
|
|
50044
|
+
categories: external_exports.array(external_exports.string().min(1)).optional().describe("Replace the categories with exactly these ([] clears them)"),
|
|
50045
|
+
addCategories: external_exports.array(external_exports.string().min(1)).optional().describe("Categories to add"),
|
|
50046
|
+
removeCategories: external_exports.array(external_exports.string().min(1)).optional().describe("Categories to remove"),
|
|
50047
|
+
isRead: external_exports.boolean().optional().describe("true to mark read, false to mark unread"),
|
|
50048
|
+
confirmToken: confirmTokenParam
|
|
50049
|
+
})
|
|
50050
|
+
},
|
|
50051
|
+
async ({ messageIds, flag, categories, addCategories, removeCategories, isRead, confirmToken }, ctx) => {
|
|
50052
|
+
const add = addCategories ?? [];
|
|
50053
|
+
const remove = removeCategories ?? [];
|
|
50054
|
+
const editing = add.length > 0 || remove.length > 0;
|
|
50055
|
+
if (categories !== void 0 && editing) {
|
|
50056
|
+
throw new McpToolError("Pass `categories` or `addCategories`/`removeCategories`, not both.", {
|
|
50057
|
+
hint: "`categories` replaces the whole list; the add/remove pair edits the current one."
|
|
50058
|
+
});
|
|
50059
|
+
}
|
|
50060
|
+
const fixed = {
|
|
50061
|
+
...isRead !== void 0 ? { IsRead: isRead } : {},
|
|
50062
|
+
...flag ? { Flag: { FlagStatus: FLAG_STATUS[flag] } } : {},
|
|
50063
|
+
...categories !== void 0 ? { Categories: categories } : {}
|
|
50064
|
+
};
|
|
50065
|
+
if (Object.keys(fixed).length === 0 && !editing) {
|
|
50066
|
+
throw new McpToolError("Nothing to update.", {
|
|
50067
|
+
hint: "Pass at least one of flag, isRead, categories, addCategories or removeCategories."
|
|
50068
|
+
});
|
|
50069
|
+
}
|
|
50070
|
+
const ids = [...new Set(messageIds)];
|
|
50071
|
+
const pathOf = (id) => `/me/messages/${encodeURIComponent(id)}`;
|
|
50072
|
+
const changes = [
|
|
50073
|
+
...flag ? [`flag ${flag}`] : [],
|
|
50074
|
+
...isRead !== void 0 ? [isRead ? "mark read" : "mark unread"] : [],
|
|
50075
|
+
...categories !== void 0 ? [`set categories [${categories.join(", ")}]`] : [],
|
|
50076
|
+
...add.length ? [`add categories [${add.join(", ")}]`] : [],
|
|
50077
|
+
...remove.length ? [`remove categories [${remove.join(", ")}]`] : []
|
|
50078
|
+
];
|
|
50079
|
+
const gate = await confirmWrite(ctx, {
|
|
50080
|
+
tool: "outlook_update_message",
|
|
50081
|
+
action: "mail.update",
|
|
50082
|
+
message: "Review and confirm this change:",
|
|
50083
|
+
summary: `Update ${ids.length} message${ids.length === 1 ? "" : "s"}: ${changes.join("; ")}`,
|
|
50084
|
+
// One signed-in mailbox per server process.
|
|
50085
|
+
account: void 0,
|
|
50086
|
+
target: ids.join(","),
|
|
50087
|
+
request: {
|
|
50088
|
+
method: "PATCH",
|
|
50089
|
+
path: ids.length === 1 ? pathOf(ids[0]) : "/me/messages/{id}",
|
|
50090
|
+
body: editing ? { ...fixed, Categories: `(each message's current categories${add.length ? ` + ${add.join(", ")}` : ""}${remove.length ? ` - ${remove.join(", ")}` : ""})` } : fixed
|
|
50091
|
+
},
|
|
50092
|
+
confirmToken
|
|
50093
|
+
});
|
|
50094
|
+
if (gate) return gate;
|
|
50095
|
+
const results = await mapWithConcurrency(ids, UPDATE_CONCURRENCY, async (id) => {
|
|
50096
|
+
try {
|
|
50097
|
+
const payload = { ...fixed };
|
|
50098
|
+
if (editing) {
|
|
50099
|
+
const current = await client2.get(`${pathOf(id)}?$select=Categories`);
|
|
50100
|
+
payload.Categories = editCategories(current?.Categories, add, remove);
|
|
50101
|
+
}
|
|
50102
|
+
const echoed = await client2.write("PATCH", pathOf(id), payload);
|
|
50103
|
+
const after = echoed && typeof echoed === "object" ? echoed : await client2.get(`${pathOf(id)}?$select=IsRead,Flag,Categories`) ?? {};
|
|
50104
|
+
const unchanged = unchangedFields(payload, after);
|
|
50105
|
+
return unchanged.length ? { id, ok: false, error: `Outlook accepted the write but these did not change: ${unchanged.join(", ")}.` } : { id, ok: true };
|
|
50106
|
+
} catch (e) {
|
|
50107
|
+
if (isCredentialFailure(e)) throw e;
|
|
50108
|
+
return { id, ok: false, error: e instanceof Error ? e.message : String(e) };
|
|
50109
|
+
}
|
|
50110
|
+
});
|
|
50111
|
+
const updated = results.filter((r) => r.ok).length;
|
|
50112
|
+
return minifiedResult({ updated, failed: results.length - updated, results });
|
|
50113
|
+
}
|
|
50114
|
+
);
|
|
49209
50115
|
server.registerTool(
|
|
49210
50116
|
"outlook_move_message",
|
|
49211
50117
|
{
|
|
49212
50118
|
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,
|
|
49213
|
-
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
50119
|
+
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
49214
50120
|
inputSchema: external_exports.object({
|
|
49215
50121
|
id: external_exports.string().min(1).describe("Message Id"),
|
|
49216
50122
|
destination: external_exports.string().min(1).describe('Destination folder id or well-known name (e.g. "archive")'),
|
|
@@ -49248,20 +50154,9 @@ function registerWriteTools(server, client2) {
|
|
|
49248
50154
|
server.registerTool(
|
|
49249
50155
|
"outlook_create_event",
|
|
49250
50156
|
{
|
|
49251
|
-
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. ' + CONFIRM_FLOW_SENTENCE + " " + CONFIRM_INJECTION_RULE,
|
|
49252
|
-
annotations: { readOnlyHint: false, destructiveHint:
|
|
49253
|
-
inputSchema:
|
|
49254
|
-
subject: external_exports.string().describe("Event title"),
|
|
49255
|
-
start: external_exports.string().min(1).describe("Start, ISO 8601 local time e.g. 2026-09-22T15:00:00"),
|
|
49256
|
-
end: external_exports.string().min(1).describe("End, ISO 8601 local time"),
|
|
49257
|
-
timeZone: external_exports.string().optional().describe("Windows time-zone name. Defaults to the MAILBOX time zone."),
|
|
49258
|
-
location: external_exports.string().optional().describe("Location display name"),
|
|
49259
|
-
body: external_exports.string().optional().describe("Event description"),
|
|
49260
|
-
attendees: recipientList.describe("Required attendees' email addresses"),
|
|
49261
|
-
optionalAttendees: recipientList.describe("Optional attendees' email addresses"),
|
|
49262
|
-
teamsMeeting: teamsMeetingParam,
|
|
49263
|
-
confirmToken: confirmTokenParam
|
|
49264
|
-
})
|
|
50157
|
+
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,
|
|
50158
|
+
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
|
|
50159
|
+
inputSchema: createEventInput
|
|
49265
50160
|
},
|
|
49266
50161
|
async ({ subject, start, end, timeZone, location, body, attendees, optionalAttendees, teamsMeeting, confirmToken }, ctx) => {
|
|
49267
50162
|
const wantTeams = teamsMeeting !== false;
|
|
@@ -49269,7 +50164,7 @@ function registerWriteTools(server, client2) {
|
|
|
49269
50164
|
...(attendees ?? []).map((a) => attendee(a, "Required")),
|
|
49270
50165
|
...(optionalAttendees ?? []).map((a) => attendee(a, "Optional"))
|
|
49271
50166
|
];
|
|
49272
|
-
const tz = timeZone ?? await mailboxTimeZone(client2) ?? "UTC";
|
|
50167
|
+
const tz = timeZone ?? await mailboxTimeZone(client2, { required: true }) ?? "UTC";
|
|
49273
50168
|
const payload = {
|
|
49274
50169
|
Subject: subject,
|
|
49275
50170
|
Start: { DateTime: start, TimeZone: tz },
|
|
@@ -49290,11 +50185,12 @@ function registerWriteTools(server, client2) {
|
|
|
49290
50185
|
confirmToken
|
|
49291
50186
|
});
|
|
49292
50187
|
if (gate) return gate;
|
|
49293
|
-
const
|
|
49294
|
-
"POST",
|
|
49295
|
-
"
|
|
49296
|
-
payload
|
|
50188
|
+
const write = await writeOrUnknown(
|
|
50189
|
+
() => client2.write("POST", "/me/events", payload),
|
|
50190
|
+
"the calendar (outlook_list_events)"
|
|
49297
50191
|
);
|
|
50192
|
+
if (!write.ok) return write.result;
|
|
50193
|
+
const created = write.value;
|
|
49298
50194
|
let joinUrl = joinUrlOf(created);
|
|
49299
50195
|
if (wantTeams && !joinUrl && created?.Id) {
|
|
49300
50196
|
const after = await client2.get(
|
|
@@ -49306,6 +50202,7 @@ function registerWriteTools(server, client2) {
|
|
|
49306
50202
|
created: true,
|
|
49307
50203
|
Id: created?.Id,
|
|
49308
50204
|
WebLink: created?.WebLink,
|
|
50205
|
+
TimeZone: tz,
|
|
49309
50206
|
...joinUrl ? { JoinUrl: joinUrl } : {},
|
|
49310
50207
|
...wantTeams && !joinUrl ? { warning: "The event was created, but Outlook returned no Teams join link." } : {}
|
|
49311
50208
|
});
|
|
@@ -49315,7 +50212,7 @@ function registerWriteTools(server, client2) {
|
|
|
49315
50212
|
"outlook_update_event",
|
|
49316
50213
|
{
|
|
49317
50214
|
description: "Update a meeting you organize: retitle, move, relocate, edit the description, or add/remove attendees. Moving only `start` keeps the meeting's length. A Microsoft Teams meeting is added if the event lacks one (`teamsMeeting: false` to skip). 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,
|
|
49318
|
-
annotations: { readOnlyHint: false, destructiveHint:
|
|
50215
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
|
|
49319
50216
|
inputSchema: external_exports.object({
|
|
49320
50217
|
id: external_exports.string().min(1).describe("Event Id from outlook_list_events or outlook_get_event"),
|
|
49321
50218
|
subject: external_exports.string().optional().describe("New title"),
|
|
@@ -49332,7 +50229,7 @@ function registerWriteTools(server, client2) {
|
|
|
49332
50229
|
})
|
|
49333
50230
|
},
|
|
49334
50231
|
async (args, ctx) => {
|
|
49335
|
-
const tz = args.timeZone ?? await mailboxTimeZone(client2) ?? "UTC";
|
|
50232
|
+
const tz = args.timeZone ?? await mailboxTimeZone(client2, { required: true }) ?? "UTC";
|
|
49336
50233
|
const path = `/me/events/${encodeURIComponent(args.id)}`;
|
|
49337
50234
|
const readEvent = (zone) => client2.get(`${path}?$select=${EVENT_STATE_SELECT}`, {
|
|
49338
50235
|
prefer: `outlook.timezone="${zone}"`
|
|
@@ -49427,6 +50324,282 @@ function registerWriteTools(server, client2) {
|
|
|
49427
50324
|
});
|
|
49428
50325
|
}
|
|
49429
50326
|
);
|
|
50327
|
+
server.registerTool(
|
|
50328
|
+
"outlook_respond_to_invite",
|
|
50329
|
+
{
|
|
50330
|
+
description: "Accept, tentatively accept or decline a meeting invitation. Pass the invite's `messageId` (from outlook_get_unread, 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. " + CONFIRM_FLOW_SENTENCE + " " + TIMEOUT_SENTENCE + " " + CONFIRM_INJECTION_RULE,
|
|
50331
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
50332
|
+
inputSchema: external_exports.object({
|
|
50333
|
+
messageId: external_exports.string().min(1).optional().describe("Id of the invite message (resolved to its event). Give this or eventId."),
|
|
50334
|
+
eventId: external_exports.string().min(1).optional().describe("Event Id. Give this or messageId."),
|
|
50335
|
+
response: external_exports.enum(["accept", "tentative", "decline"]).describe("Your answer"),
|
|
50336
|
+
comment: external_exports.string().optional().describe("Note to the organizer sent with the response"),
|
|
50337
|
+
sendResponse: external_exports.boolean().optional().describe("Tell the organizer (default true). false changes only your calendar."),
|
|
50338
|
+
confirmToken: confirmTokenParam
|
|
50339
|
+
})
|
|
50340
|
+
},
|
|
50341
|
+
async ({ messageId, eventId, response, comment, sendResponse, confirmToken }, ctx) => {
|
|
50342
|
+
if (messageId === void 0 === (eventId === void 0)) {
|
|
50343
|
+
throw new McpToolError("Pass exactly one of messageId or eventId.", {
|
|
50344
|
+
hint: "messageId is the invite in your inbox; eventId is the meeting on your calendar."
|
|
50345
|
+
});
|
|
50346
|
+
}
|
|
50347
|
+
let event;
|
|
50348
|
+
if (messageId !== void 0) {
|
|
50349
|
+
const msg = await client2.get(
|
|
50350
|
+
`/me/messages/${encodeURIComponent(messageId)}`,
|
|
50351
|
+
{ query: { $select: `Id,${MEETING_MESSAGE_TYPE}`, $expand: EVENT_EXPAND } }
|
|
50352
|
+
);
|
|
50353
|
+
event = resolvedEvent(msg?.Event);
|
|
50354
|
+
if (!event) {
|
|
50355
|
+
if (isMeetingMessage(msg)) {
|
|
50356
|
+
throw new McpToolError(
|
|
50357
|
+
"This invite's meeting is no longer on your calendar (already declined, cancelled or deleted) \u2014 there is nothing to respond to.",
|
|
50358
|
+
{ hint: "Mark the invite read or file it; answering it again is not possible." }
|
|
50359
|
+
);
|
|
50360
|
+
}
|
|
50361
|
+
throw new McpToolError("That message is not a meeting invite.", {
|
|
50362
|
+
hint: 'Only messages outlook_get_unread marks kind "meetingRequest" carry an event to answer.'
|
|
50363
|
+
});
|
|
50364
|
+
}
|
|
50365
|
+
} else {
|
|
50366
|
+
event = await client2.get(
|
|
50367
|
+
`/me/events/${encodeURIComponent(eventId)}?$select=${INVITE_EVENT_SELECT}`
|
|
50368
|
+
);
|
|
50369
|
+
}
|
|
50370
|
+
const id = event?.Id ?? eventId;
|
|
50371
|
+
if (event?.IsCancelled === true) {
|
|
50372
|
+
throw new McpToolError("This meeting has been cancelled; there is nothing to respond to.", {
|
|
50373
|
+
hint: "Remove it from your calendar with outlook_delete_event if it is still shown."
|
|
50374
|
+
});
|
|
50375
|
+
}
|
|
50376
|
+
if (event?.IsOrganizer === true || event?.ResponseStatus?.Response === "Organizer") {
|
|
50377
|
+
throw new McpToolError("You organize this meeting, so you cannot respond to it.", {
|
|
50378
|
+
hint: "To change or call it off, use outlook_update_event or Outlook itself."
|
|
50379
|
+
});
|
|
50380
|
+
}
|
|
50381
|
+
const { verb, status } = INVITE_RESPONSES[response];
|
|
50382
|
+
const notify = sendResponse !== false;
|
|
50383
|
+
if (!notify && comment) {
|
|
50384
|
+
throw new McpToolError("A comment can only go with a response the organizer receives.", {
|
|
50385
|
+
hint: "Drop the comment, or leave sendResponse at its default (true) so the organizer gets both."
|
|
50386
|
+
});
|
|
50387
|
+
}
|
|
50388
|
+
const path = `/me/events/${encodeURIComponent(id)}/${verb}`;
|
|
50389
|
+
const payload = { ...comment ? { Comment: comment } : {}, SendResponse: notify };
|
|
50390
|
+
if (notify) {
|
|
50391
|
+
const when = `${trimTime2(event?.Start?.DateTime) ?? "?"} to ${trimTime2(event?.End?.DateTime) ?? "?"}${event?.Start?.TimeZone ? ` (${event.Start.TimeZone})` : ""}`;
|
|
50392
|
+
const organizer = event?.Organizer?.EmailAddress?.Address ?? "the organizer";
|
|
50393
|
+
const gate = await confirmWrite(ctx, {
|
|
50394
|
+
tool: "outlook_respond_to_invite",
|
|
50395
|
+
action: "calendar.respond_to_invite",
|
|
50396
|
+
message: "Review and confirm this response (the organizer is notified):",
|
|
50397
|
+
summary: `${response === "tentative" ? "Tentatively accept" : response === "accept" ? "Accept" : "Decline"} "${event?.Subject ?? id}" ${when}, organized by ${organizer}` + (comment ? `, with comment: "${comment}"` : ""),
|
|
50398
|
+
// One signed-in mailbox per server process.
|
|
50399
|
+
account: void 0,
|
|
50400
|
+
target: id,
|
|
50401
|
+
request: { method: "POST", path, body: payload },
|
|
50402
|
+
confirmToken
|
|
50403
|
+
});
|
|
50404
|
+
if (gate) return gate;
|
|
50405
|
+
}
|
|
50406
|
+
const write = await writeOrUnknown(
|
|
50407
|
+
() => client2.write("POST", path, payload),
|
|
50408
|
+
"the event's response (outlook_get_event)"
|
|
50409
|
+
);
|
|
50410
|
+
if (!write.ok) return write.result;
|
|
50411
|
+
let after;
|
|
50412
|
+
try {
|
|
50413
|
+
after = await client2.get(
|
|
50414
|
+
`/me/events/${encodeURIComponent(id)}?$select=ResponseStatus`
|
|
50415
|
+
);
|
|
50416
|
+
} catch (e) {
|
|
50417
|
+
if (response === "decline" && errorStatusOf(e) === 404) {
|
|
50418
|
+
return minifiedResult({
|
|
50419
|
+
responded: true,
|
|
50420
|
+
eventId: id,
|
|
50421
|
+
response: status,
|
|
50422
|
+
sentResponse: notify,
|
|
50423
|
+
note: "The declined meeting is no longer on your calendar."
|
|
50424
|
+
});
|
|
50425
|
+
}
|
|
50426
|
+
return minifiedResult({
|
|
50427
|
+
responded: null,
|
|
50428
|
+
eventId: id,
|
|
50429
|
+
sentResponse: notify,
|
|
50430
|
+
warning: `Outlook accepted the response but re-reading the event failed (${e instanceof Error ? e.message : String(e)}), so it is unverified.`
|
|
50431
|
+
});
|
|
50432
|
+
}
|
|
50433
|
+
const got = after?.ResponseStatus?.Response;
|
|
50434
|
+
return minifiedResult({
|
|
50435
|
+
responded: got === status,
|
|
50436
|
+
eventId: id,
|
|
50437
|
+
response: got,
|
|
50438
|
+
sentResponse: notify,
|
|
50439
|
+
...got === status ? {} : { warning: `Outlook accepted the response but the status did not change (still ${got ?? "unknown"}).` }
|
|
50440
|
+
});
|
|
50441
|
+
}
|
|
50442
|
+
);
|
|
50443
|
+
server.registerTool(
|
|
50444
|
+
"outlook_delete_event",
|
|
50445
|
+
{
|
|
50446
|
+
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,
|
|
50447
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
|
|
50448
|
+
inputSchema: external_exports.object({
|
|
50449
|
+
id: external_exports.string().min(1).describe("Event Id from outlook_list_events or outlook_get_event (an occurrence, or a series master)"),
|
|
50450
|
+
confirmToken: confirmTokenParam
|
|
50451
|
+
})
|
|
50452
|
+
},
|
|
50453
|
+
async ({ id, confirmToken }, ctx) => {
|
|
50454
|
+
const path = `/me/events/${encodeURIComponent(id)}`;
|
|
50455
|
+
const zone = await mailboxTimeZone(client2);
|
|
50456
|
+
const event = await client2.get(
|
|
50457
|
+
`${path}?$select=${DELETE_EVENT_SELECT}`,
|
|
50458
|
+
zone ? { prefer: `outlook.timezone="${zone}"` } : {}
|
|
50459
|
+
);
|
|
50460
|
+
const organizer = event?.IsOrganizer === true || event?.ResponseStatus?.Response === "Organizer";
|
|
50461
|
+
const cancelled = event?.IsCancelled === true;
|
|
50462
|
+
if (!organizer && !cancelled) {
|
|
50463
|
+
throw new McpToolError("Only the organizer can delete this meeting.", {
|
|
50464
|
+
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.'
|
|
50465
|
+
});
|
|
50466
|
+
}
|
|
50467
|
+
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";
|
|
50468
|
+
const attendeeCount = event?.Attendees?.length ?? 0;
|
|
50469
|
+
const notifies = organizer && !cancelled && attendeeCount > 0;
|
|
50470
|
+
if (notifies) {
|
|
50471
|
+
const when = `${trimTime2(event?.Start?.DateTime) ?? "?"} to ${trimTime2(event?.End?.DateTime) ?? "?"}${event?.Start?.TimeZone ? ` (${event.Start.TimeZone})` : ""}`;
|
|
50472
|
+
const gate = await confirmWrite(ctx, {
|
|
50473
|
+
tool: "outlook_delete_event",
|
|
50474
|
+
action: "calendar.delete_event",
|
|
50475
|
+
message: "Review and confirm this deletion (every attendee is sent a cancellation):",
|
|
50476
|
+
summary: `Delete "${event?.Subject ?? id}" ${when} \u2014 ${scope}; ${attendeeCount} attendee${attendeeCount === 1 ? "" : "s"} will be sent a cancellation`,
|
|
50477
|
+
// One signed-in mailbox per server process.
|
|
50478
|
+
account: void 0,
|
|
50479
|
+
target: id,
|
|
50480
|
+
request: { method: "DELETE", path },
|
|
50481
|
+
preview: { scope, attendees: attendeeCount },
|
|
50482
|
+
confirmToken
|
|
50483
|
+
});
|
|
50484
|
+
if (gate) return gate;
|
|
50485
|
+
}
|
|
50486
|
+
const write = await writeOrUnknown(
|
|
50487
|
+
() => client2.write("DELETE", path),
|
|
50488
|
+
"the calendar (outlook_get_event)"
|
|
50489
|
+
);
|
|
50490
|
+
if (!write.ok) return write.result;
|
|
50491
|
+
const base = { Id: id, Subject: event?.Subject, scope, cancellationSent: notifies };
|
|
50492
|
+
let after;
|
|
50493
|
+
try {
|
|
50494
|
+
after = await client2.get(`${path}?$select=Id`);
|
|
50495
|
+
} catch (e) {
|
|
50496
|
+
if (errorStatusOf(e) === 404) return minifiedResult({ deleted: true, ...base });
|
|
50497
|
+
return minifiedResult({
|
|
50498
|
+
deleted: null,
|
|
50499
|
+
...base,
|
|
50500
|
+
warning: `Outlook accepted the delete but re-reading the event failed (${e instanceof Error ? e.message : String(e)}), so it is unverified.`
|
|
50501
|
+
});
|
|
50502
|
+
}
|
|
50503
|
+
return minifiedResult({
|
|
50504
|
+
deleted: false,
|
|
50505
|
+
...base,
|
|
50506
|
+
warning: `Outlook accepted the delete but the event is still on the calendar${after?.Id ? ` (${after.Id})` : ""}.`
|
|
50507
|
+
});
|
|
50508
|
+
}
|
|
50509
|
+
);
|
|
50510
|
+
server.registerTool(
|
|
50511
|
+
"outlook_reply",
|
|
50512
|
+
{
|
|
50513
|
+
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,
|
|
50514
|
+
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
|
|
50515
|
+
inputSchema: external_exports.object({
|
|
50516
|
+
messageId: external_exports.string().min(1).describe("Id of the message to answer or forward"),
|
|
50517
|
+
mode: external_exports.enum(["reply", "replyAll", "forward"]).describe("reply (sender only), replyAll, or forward"),
|
|
50518
|
+
comment: external_exports.string().describe("Your text, placed above the quoted original"),
|
|
50519
|
+
to: recipientList.describe("Forward recipients' email addresses (forward only, required there)"),
|
|
50520
|
+
draftOnly: external_exports.boolean().optional().describe("Save to Drafts instead of sending (default false)"),
|
|
50521
|
+
confirmToken: confirmTokenParam
|
|
50522
|
+
})
|
|
50523
|
+
},
|
|
50524
|
+
async ({ messageId, mode, comment, to, draftOnly, confirmToken }, ctx) => {
|
|
50525
|
+
if (mode === "forward" && !to?.length) {
|
|
50526
|
+
throw new McpToolError("A forward needs at least one recipient.", {
|
|
50527
|
+
hint: "Pass `to` with the addresses to forward to."
|
|
50528
|
+
});
|
|
50529
|
+
}
|
|
50530
|
+
if (mode !== "forward" && to?.length) {
|
|
50531
|
+
throw new McpToolError("`to` applies only to a forward.", {
|
|
50532
|
+
hint: 'A reply goes where Outlook computes; use mode "forward" to send it somewhere else, or outlook_send_mail.'
|
|
50533
|
+
});
|
|
50534
|
+
}
|
|
50535
|
+
const { send, draft, prefix } = REPLY_MODES[mode];
|
|
50536
|
+
const base = `/me/messages/${encodeURIComponent(messageId)}`;
|
|
50537
|
+
const forwardTo = mode === "forward" ? { ToRecipients: toRecipients(to) } : {};
|
|
50538
|
+
if (draftOnly === true) {
|
|
50539
|
+
const create = await writeOrUnknown(
|
|
50540
|
+
() => client2.write(
|
|
50541
|
+
"POST",
|
|
50542
|
+
`${base}/${draft}`,
|
|
50543
|
+
{}
|
|
50544
|
+
),
|
|
50545
|
+
"Drafts"
|
|
50546
|
+
);
|
|
50547
|
+
if (!create.ok) return create.result;
|
|
50548
|
+
const created = create.value;
|
|
50549
|
+
if (!created?.Id) {
|
|
50550
|
+
throw new McpToolError("Outlook did not return the draft it created.", {
|
|
50551
|
+
hint: "Check the Drafts folder before trying again."
|
|
50552
|
+
});
|
|
50553
|
+
}
|
|
50554
|
+
try {
|
|
50555
|
+
await client2.write("PATCH", `/me/messages/${encodeURIComponent(created.Id)}`, {
|
|
50556
|
+
Body: bodyWithComment(created.Body, comment),
|
|
50557
|
+
...forwardTo
|
|
50558
|
+
});
|
|
50559
|
+
} catch (e) {
|
|
50560
|
+
return minifiedResult({
|
|
50561
|
+
drafted: true,
|
|
50562
|
+
mode,
|
|
50563
|
+
draftId: created.Id,
|
|
50564
|
+
WebLink: created.WebLink,
|
|
50565
|
+
warning: `The draft was created but adding your comment${mode === "forward" ? " and recipients" : ""} failed (${e instanceof Error ? e.message : String(e)}). Edit it in Drafts, or delete it before trying again.`
|
|
50566
|
+
});
|
|
50567
|
+
}
|
|
50568
|
+
return minifiedResult({ drafted: true, mode, draftId: created.Id, WebLink: created.WebLink });
|
|
50569
|
+
}
|
|
50570
|
+
const orig = await client2.get(base, {
|
|
50571
|
+
query: { $select: "Subject,From,ReplyTo,ToRecipients,CcRecipients" }
|
|
50572
|
+
});
|
|
50573
|
+
const sender = orig?.ReplyTo?.length ? orig.ReplyTo : orig?.From ? [orig.From] : [];
|
|
50574
|
+
const recipients = mode === "forward" ? { to: to ?? [], cc: [] } : mode === "replyAll" ? { to: addresses(sender, orig?.ToRecipients), cc: addresses(orig?.CcRecipients) } : { to: addresses(sender), cc: [] };
|
|
50575
|
+
const subject = `${prefix}${orig?.Subject ?? ""}`;
|
|
50576
|
+
const path = `${base}/${send}`;
|
|
50577
|
+
const payload = { Comment: comment, ...forwardTo };
|
|
50578
|
+
const everyone = [...recipients.to, ...recipients.cc.map((c) => `cc ${c}`)];
|
|
50579
|
+
const who = everyone.length > MAX_RECIPIENTS ? `${everyone.slice(0, MAX_RECIPIENTS).join(", ")} and ${(everyone.length - MAX_RECIPIENTS).toLocaleString("en-US")} more` : everyone.join(", ");
|
|
50580
|
+
const gate = await confirmWrite(ctx, {
|
|
50581
|
+
tool: "outlook_reply",
|
|
50582
|
+
action: `mail.${send}`,
|
|
50583
|
+
message: "Review and confirm this email before it is sent:",
|
|
50584
|
+
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}"`,
|
|
50585
|
+
// One signed-in mailbox per server process.
|
|
50586
|
+
account: void 0,
|
|
50587
|
+
target: messageId,
|
|
50588
|
+
request: { method: "POST", path, body: payload },
|
|
50589
|
+
confirmToken
|
|
50590
|
+
});
|
|
50591
|
+
if (gate) return gate;
|
|
50592
|
+
const sent = await writeOrUnknown(() => client2.write("POST", path, payload), "Sent Items");
|
|
50593
|
+
if (!sent.ok) return sent.result;
|
|
50594
|
+
return mailboxUntrusted({
|
|
50595
|
+
sent: true,
|
|
50596
|
+
mode,
|
|
50597
|
+
subject,
|
|
50598
|
+
...cappedRecipients("to", recipients.to),
|
|
50599
|
+
...cappedRecipients("cc", recipients.cc)
|
|
50600
|
+
});
|
|
50601
|
+
}
|
|
50602
|
+
);
|
|
49430
50603
|
}
|
|
49431
50604
|
|
|
49432
50605
|
// src/tools/healthcheck.ts
|