@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/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 = args === void 0 ? void 0 : boundArgs(args);
36149
- const bindingAdded = bound === void 0 || rest.binding ? rest : { ...rest, binding: { key: confirmKeyFromEnv(env), args: bound, ttlSeconds: ttlSeconds ?? DEFAULT_TTL_SECONDS2 } };
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 = bound === void 0 ? subject : async () => {
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
- ...account === void 0 ? {} : { account },
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
- throw err;
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 == null || timeoutMs <= 0) {
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
- try {
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
- if (err instanceof RequestTimeoutError) {
36744
- attempt.res.body?.cancel().catch(() => {
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
- async function send(method, path, opt) {
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 fetchWith = (token, signal) => doFetch(url2, {
36782
- method,
36783
- headers: {
36784
- Accept: "application/json",
36785
- ...contentType !== void 0 ? { "Content-Type": contentType } : {},
36786
- ...opts.baseHeaders,
36787
- ...authHeader(token || void 0),
36788
- ...opt.headers
36789
- },
36790
- ...signal ? { signal } : {},
36791
- ...bodyInit
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 readBody(attempt, (r) => r.text()).catch(() => "");
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 readBody(attempt, (r) => r.text()).catch(() => "");
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 fetchJson(method, path, opt = {}) {
36842
- const attempt = await send(method, path, opt);
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 void 0;
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, text, method, path, service);
37223
+ throw httpError(res, await readErrorText(attempt, maxBytes), method, path, service);
36855
37224
  }
36856
- if (text.length === 0)
36857
- return void 0;
36858
- return JSON.parse(text);
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 fetchHtml(method, path, opt = {}) {
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, text, method, path, service);
37245
+ throw httpError(res, await readErrorText(attempt, maxBytes), method, path, service);
36871
37246
  }
36872
- return text;
37247
+ return readText(attempt, maxBytes);
36873
37248
  }
36874
- async function fetchRaw(method, path, opt = {}) {
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 readBody(attempt, (r) => r.text()).catch((err) => {
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 = new Uint8Array(await readBody(attempt, (r) => r.arrayBuffer()));
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.4.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 parseJsonBody(response) {
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 parseJsonBody(response);
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 parseJsonBody(response);
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 = parseJsonBody(response);
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 from '${source}'. It is present but no longer valid \u2014 most often expired or revoked upstream. Re-authenticate and reconnect; retrying will not fix it.`;
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 from '${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.`;
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 classified = classifyThrown?.(e) ?? (edge !== null ? { kind: "edge_blocked", detail: { vendor: edge.vendor } } : void 0);
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 = statusOf2(e);
47697
+ status = errorStatusOf(e);
47260
47698
  const aborted2 = e instanceof Error && e.name === "AbortError";
47261
47699
  const edge = edgeBlockOf(e);
47262
- arm = edge !== null ? "edge_blocked" : 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";
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 Error("TokenManager: cannot refresh \u2014 no refresh token is available.");
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: addrs(m.ToRecipients),
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: addrs(m.CcRecipients),
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 || void 0,
48493
- IsCancelled: e.IsCancelled || void 0
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
- ShowAs: e.ShowAs,
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
- return nextLink !== void 0 ? client2.getAbsolute(nextLink, opts) : client2.get(path, opts);
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
- `/me/mailfolders${qs({ $top: limit ?? 50 })}`
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: "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. " + UNTRUSTED_DESCRIPTION_SUFFIX,
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
- `${base}${qs(params)}`
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${qs({
48695
- $select: "Id,Name,Size,ContentType"
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
- `/me/calendarview${qs2({
48777
- startDateTime: start,
48778
- endDateTime: end,
48779
- $select: EVENT_SELECT,
48780
- $orderby: "Start/DateTime",
48781
- $top: limit ?? 50
48782
- })}`,
48783
- zone ? { prefer: `outlook.timezone="${zone}"` } : {}
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, `/me/calendars${qs2({ $select: "Id,Name,Color,CanEdit,Owner" })}`);
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
- Start: trimTime(s.MeetingTimeSlot?.Start?.DateTime),
48876
- End: trimTime(s.MeetingTimeSlot?.End?.DateTime),
48877
- Confidence: s.Confidence,
48878
- Organizer: s.OrganizerAvailability,
48879
- Attendees: Object.fromEntries(
48880
- (s.AttendeeAvailability ?? []).map((a) => [
48881
- a.Attendee?.EmailAddress?.Address ?? a.Attendee?.EmailAddress?.Name ?? "?",
48882
- a.Availability
48883
- ])
48884
- ),
48885
- Reason: s.SuggestionReason
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
- `/me/contacts${qs3({
48996
- $top: limit ?? 50,
48997
- $skip: skip,
48998
- // `MobilePhone1`, not `MobilePhone`: the latter is the Graph name and
48999
- // the v2.0 Contact type rejects it outright with a 400.
49000
- $select: "Id,DisplayName,EmailAddresses,CompanyName,JobTitle,MobilePhone1"
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
- `/me/people${qs3({
49022
- $top: limit ?? 25,
49023
- $select: "Id,DisplayName,ScoredEmailAddresses,JobTitle,CompanyName"
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
- `/me/tasks${qs3({
49045
- $top: limit ?? 50,
49046
- $select: "Id,Subject,Status,Importance,DueDateTime,CompletedDateTime"
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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
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: false },
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: false },
49253
- inputSchema: external_exports.object({
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 created = await client2.write(
49294
- "POST",
49295
- "/me/events",
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: false, idempotentHint: true },
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