@chrischall/office-outlook-mcp 0.3.5 → 0.4.1

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
@@ -23737,10 +23737,10 @@ function scanXMcpHeaderDeclarations(inputSchema) {
23737
23737
  if (!RFC9110_TOKEN.test(raw)) return `${pathName(path)}: x-mcp-header '${raw}' is not a valid RFC 9110 token (no spaces, control characters or HTTP delimiters)`;
23738
23738
  const type = typeof schema.type === "string" ? schema.type : void 0;
23739
23739
  if (type === void 0 || !PERMITTED_X_MCP_HEADER_TYPES.has(type)) return `${pathName(path)}: x-mcp-header is only permitted on primitive-typed properties (string, integer, boolean); got ${type ?? "<none>"}`;
23740
- const lower = raw.toLowerCase();
23741
- const prior = seenLower.get(lower);
23740
+ const lower2 = raw.toLowerCase();
23741
+ const prior = seenLower.get(lower2);
23742
23742
  if (prior !== void 0) return `x-mcp-header '${raw}' is not case-insensitively unique (also declared as '${prior}')`;
23743
- seenLower.set(lower, raw);
23743
+ seenLower.set(lower2, raw);
23744
23744
  declarations.push({
23745
23745
  path,
23746
23746
  headerName: raw,
@@ -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",
@@ -35904,7 +35991,7 @@ function readEnvVar(key, opts = {}) {
35904
35991
  if (typeof raw === "string") {
35905
35992
  const trimmed = raw.trim();
35906
35993
  if (trimmed.length > 0 && trimmed !== "undefined" && trimmed !== "null" && !PLACEHOLDER_RE.test(trimmed)) {
35907
- return trimmed;
35994
+ return opts.trim === false ? raw : trimmed;
35908
35995
  }
35909
35996
  }
35910
35997
  return opts.default;
@@ -36141,12 +36228,19 @@ function boundArgs(args) {
36141
36228
  }
36142
36229
  function confirmationFromEnv(options) {
36143
36230
  const { tool, account, confirmToken, subject, instruction, args, env = process.env, spent: callerSpent, ...rest } = options;
36231
+ if (args === void 0 || args === null) {
36232
+ throw new TypeError(`confirmationFromEnv: ${tool} passed no args; pass the tool's validated arguments ({} for a tool with none).`);
36233
+ }
36144
36234
  const spent = callerSpent ?? spentTokenStoreFromEnv(env);
36145
36235
  const mode = readConfirmMode(env);
36146
36236
  const ttlSeconds = readConfirmTtl(env);
36147
36237
  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 } };
36238
+ const bound = boundArgs(args);
36239
+ const elicitationBound = account === void 0 ? { args: bound } : { account, args: bound };
36240
+ const bindingAdded = rest.binding ? rest : {
36241
+ ...rest,
36242
+ binding: { key: confirmKeyFromEnv(env), args: elicitationBound, ttlSeconds: ttlSeconds ?? DEFAULT_TTL_SECONDS2 }
36243
+ };
36150
36244
  const confirmation = elicitationOff ? { ...bindingAdded, elicitation: false } : bindingAdded;
36151
36245
  if (mode === "refuse" || ttlSeconds === void 0) {
36152
36246
  const reason = mode === "refuse" ? REFUSE_HINT : BAD_TTL_HINT;
@@ -36156,7 +36250,7 @@ function confirmationFromEnv(options) {
36156
36250
  unsupportedNote: confirmation.unsupportedNote ? `${confirmation.unsupportedNote} ${hint}` : hint
36157
36251
  };
36158
36252
  }
36159
- const boundSubject = bound === void 0 ? subject : async () => {
36253
+ const boundSubject = async () => {
36160
36254
  const read = await subject();
36161
36255
  if ("content" in read)
36162
36256
  return read;
@@ -36244,7 +36338,7 @@ async function confirmWrite(ctx, options) {
36244
36338
  ...options.unsupportedNote === void 0 ? {} : { unsupportedNote: options.unsupportedNote },
36245
36339
  ...options.confirmationLabel === void 0 ? {} : { confirmationLabel: options.confirmationLabel },
36246
36340
  tool,
36247
- ...account === void 0 ? {} : { account },
36341
+ account,
36248
36342
  confirmToken,
36249
36343
  args: bound,
36250
36344
  subject: () => ({ target, ...revision === void 0 ? {} : { revision }, payload: bound, preview }),
@@ -36272,12 +36366,14 @@ var init_merge_write = __esm({
36272
36366
 
36273
36367
  // node_modules/@chrischall/mcp-utils/dist/server/index.js
36274
36368
  function hintResultOrRethrow(err) {
36369
+ if (err instanceof ProtocolError && err.code === ProtocolErrorCode.UrlElicitationRequired)
36370
+ throw err;
36275
36371
  if (err instanceof McpToolError && err.hint) {
36276
36372
  return errorResult(`${err.message}
36277
36373
 
36278
36374
  Hint: ${err.hint}`);
36279
36375
  }
36280
- throw err;
36376
+ return errorResult(messageOf(err));
36281
36377
  }
36282
36378
  function mcpRequestFrom(args) {
36283
36379
  const ctx = args.at(-1);
@@ -36496,6 +36592,7 @@ var init_write = __esm({
36496
36592
  var init_output = __esm({
36497
36593
  "node_modules/@chrischall/mcp-utils/dist/fs/output.js"() {
36498
36594
  init_config();
36595
+ init_errors3();
36499
36596
  init_confine();
36500
36597
  init_write();
36501
36598
  }
@@ -36576,6 +36673,7 @@ var init_scrape = __esm({
36576
36673
  // node_modules/@chrischall/mcp-utils/dist/http/throttle.js
36577
36674
  var init_throttle = __esm({
36578
36675
  "node_modules/@chrischall/mcp-utils/dist/http/throttle.js"() {
36676
+ init_cancel();
36579
36677
  }
36580
36678
  });
36581
36679
 
@@ -36601,6 +36699,113 @@ var init_net_atoms = __esm({
36601
36699
  }
36602
36700
  });
36603
36701
 
36702
+ // node_modules/@chrischall/mcp-utils/dist/http/body-limit.js
36703
+ async function readBytesCapped(res, maxBytes, service, bounded) {
36704
+ if (maxBytes !== void 0) {
36705
+ const declared = Number(res.headers.get("content-length") ?? NaN);
36706
+ if (Number.isFinite(declared) && declared > maxBytes) {
36707
+ cancelStream(res.body);
36708
+ throw new ResponseTooLargeError(service, maxBytes);
36709
+ }
36710
+ }
36711
+ if (!res.body)
36712
+ return new Uint8Array(0);
36713
+ const reader = res.body.getReader();
36714
+ const chunks = [];
36715
+ let total = 0;
36716
+ try {
36717
+ for (; ; ) {
36718
+ const { done, value } = await bounded(reader.read());
36719
+ if (done)
36720
+ break;
36721
+ total += value.byteLength;
36722
+ if (maxBytes !== void 0 && total > maxBytes)
36723
+ throw new ResponseTooLargeError(service, maxBytes);
36724
+ chunks.push(value);
36725
+ }
36726
+ } catch (err) {
36727
+ reader.cancel().catch(() => {
36728
+ });
36729
+ throw err;
36730
+ }
36731
+ const out = new Uint8Array(total);
36732
+ let offset = 0;
36733
+ for (const c of chunks) {
36734
+ out.set(c, offset);
36735
+ offset += c.byteLength;
36736
+ }
36737
+ return out;
36738
+ }
36739
+ function cancelStream(body) {
36740
+ try {
36741
+ body?.cancel().catch(() => {
36742
+ });
36743
+ } catch {
36744
+ }
36745
+ }
36746
+ var ResponseTooLargeError;
36747
+ var init_body_limit = __esm({
36748
+ "node_modules/@chrischall/mcp-utils/dist/http/body-limit.js"() {
36749
+ init_errors3();
36750
+ ResponseTooLargeError = class extends McpToolError {
36751
+ maxBytes;
36752
+ constructor(service, maxBytes) {
36753
+ super(`Response from ${service} is larger than the ${maxBytes}-byte limit.`, {
36754
+ hint: "Raise maxBytes (or the client's maxResponseBytes) if a response this large is expected.",
36755
+ kind: "too_large"
36756
+ });
36757
+ this.name = "ResponseTooLargeError";
36758
+ this.maxBytes = maxBytes;
36759
+ }
36760
+ };
36761
+ }
36762
+ });
36763
+
36764
+ // node_modules/@chrischall/mcp-utils/dist/http/fetch-bounded.js
36765
+ var init_fetch_bounded = __esm({
36766
+ "node_modules/@chrischall/mcp-utils/dist/http/fetch-bounded.js"() {
36767
+ init_cancel();
36768
+ init_errors3();
36769
+ init_body_limit();
36770
+ init_http();
36771
+ }
36772
+ });
36773
+
36774
+ // node_modules/@chrischall/mcp-utils/dist/http/url-safety.js
36775
+ function isDotSegment(segment) {
36776
+ const s = segment.replace(/%2e/gi, ".");
36777
+ return s === "." || s === "..";
36778
+ }
36779
+ function pathPartOf(path) {
36780
+ const end = path.search(/[?#]/);
36781
+ return end === -1 ? path : path.slice(0, end);
36782
+ }
36783
+ function findPathHazard(path) {
36784
+ const pathPart = pathPartOf(path);
36785
+ if (/[\x00-\x1f\x7f]/.test(pathPart))
36786
+ return "control character";
36787
+ if (pathPart.endsWith(" "))
36788
+ return "trailing space";
36789
+ if (pathPart.includes("\\"))
36790
+ return "backslash";
36791
+ if (pathPart.split("/").some(isDotSegment))
36792
+ return "dot segment";
36793
+ return void 0;
36794
+ }
36795
+ var RedirectRefusedError;
36796
+ var init_url_safety = __esm({
36797
+ "node_modules/@chrischall/mcp-utils/dist/http/url-safety.js"() {
36798
+ init_errors3();
36799
+ init_config();
36800
+ RedirectRefusedError = class extends McpToolError {
36801
+ constructor(message) {
36802
+ super(message);
36803
+ this.name = "RedirectRefusedError";
36804
+ }
36805
+ };
36806
+ }
36807
+ });
36808
+
36604
36809
  // node_modules/@chrischall/mcp-utils/dist/internal/retry-after.js
36605
36810
  function retryAfterToMs(header, now = Date.now()) {
36606
36811
  if (header == null)
@@ -36621,6 +36826,14 @@ var init_retry_after = __esm({
36621
36826
  });
36622
36827
 
36623
36828
  // node_modules/@chrischall/mcp-utils/dist/http/index.js
36829
+ function cancelBody(res) {
36830
+ try {
36831
+ const body = res.body;
36832
+ body?.cancel?.()?.catch(() => {
36833
+ });
36834
+ } catch {
36835
+ }
36836
+ }
36624
36837
  function headerOf(headers, name) {
36625
36838
  if (headers === void 0)
36626
36839
  return void 0;
@@ -36646,6 +36859,49 @@ function detectEdgeBlock(input2) {
36646
36859
  }
36647
36860
  return null;
36648
36861
  }
36862
+ function jsonKind(value) {
36863
+ if (value === null)
36864
+ return "null";
36865
+ if (Array.isArray(value))
36866
+ return "array";
36867
+ return typeof value;
36868
+ }
36869
+ function parseJsonBody(text, opts = {}) {
36870
+ const contentType = opts.contentType ?? headerOf(opts.headers, "content-type");
36871
+ const details = {
36872
+ service: opts.service,
36873
+ method: opts.method,
36874
+ path: opts.path,
36875
+ status: opts.status,
36876
+ contentType,
36877
+ expected: opts.expect
36878
+ };
36879
+ if (text.trim().length === 0) {
36880
+ if (opts.expect === void 0)
36881
+ return void 0;
36882
+ throw new UpstreamFormatError({ ...details, received: "empty" });
36883
+ }
36884
+ let value;
36885
+ try {
36886
+ value = JSON.parse(text);
36887
+ } catch (cause) {
36888
+ const edge = detectEdgeBlock({ body: text, headers: opts.headers, status: opts.status });
36889
+ if (edge) {
36890
+ throw new EdgeBlockedError(opts.status ?? 200, edge.vendor, {
36891
+ service: opts.service ?? "the upstream service",
36892
+ method: opts.method,
36893
+ path: opts.path
36894
+ });
36895
+ }
36896
+ throw new UpstreamFormatError({ ...details, expected: void 0, received: "non-json", cause });
36897
+ }
36898
+ if (opts.expect !== void 0) {
36899
+ const kind = jsonKind(value);
36900
+ if (kind !== opts.expect)
36901
+ throw new UpstreamFormatError({ ...details, received: kind });
36902
+ }
36903
+ return value;
36904
+ }
36649
36905
  function httpError(res, text, method, path, service) {
36650
36906
  const edge = detectEdgeBlock({ body: text, headers: res.headers, status: res.status });
36651
36907
  if (edge)
@@ -36696,13 +36952,26 @@ function createApiClient(opts) {
36696
36952
  const sleep3 = opts.sleep ?? defaultSleep;
36697
36953
  const unauthorized = () => opts.onUnauthorized ? opts.onUnauthorized() : new UnauthorizedError(service);
36698
36954
  const retryStatuses = retry.statuses ?? [429];
36699
- const timeoutMs = opts.timeout;
36955
+ const timeoutMs = opts.timeout === false ? 0 : opts.timeout ?? DEFAULT_REQUEST_TIMEOUT_MS;
36956
+ const redirectMode = opts.redirect;
36957
+ const maxRedirects = opts.maxRedirects ?? DEFAULT_MAX_REDIRECTS;
36958
+ const reportWriteOutcome = opts.writeOutcomeUnknown !== false;
36959
+ const defaultMaxBytes = checkMaxBytes("maxResponseBytes", opts.maxResponseBytes);
36960
+ if (!Number.isSafeInteger(maxRedirects) || maxRedirects < 0) {
36961
+ throw new TypeError(`createApiClient: maxRedirects must be a non-negative integer, got ${String(maxRedirects)}.`);
36962
+ }
36700
36963
  const authHeader = (token) => !token ? {} : opts.tokenHeader ? { [opts.tokenHeader]: token } : { Authorization: `Bearer ${token}` };
36964
+ const transportFailures = /* @__PURE__ */ new WeakSet();
36965
+ const markTransportFailure = (err) => {
36966
+ if (typeof err === "object" && err !== null && err.name !== "AbortError") {
36967
+ transportFailures.add(err);
36968
+ }
36969
+ };
36701
36970
  async function withTimeout(run) {
36702
- if (timeoutMs == null || timeoutMs <= 0) {
36971
+ if (timeoutMs <= 0) {
36703
36972
  const res2 = await run(withAmbientCancellation(void 0));
36704
36973
  return { res: res2, done: () => {
36705
- }, expired: void 0 };
36974
+ }, expired: void 0, timedOut: () => false };
36706
36975
  }
36707
36976
  const controller = new AbortController();
36708
36977
  const timer = setTimeout(() => controller.abort(), timeoutMs);
@@ -36723,16 +36992,11 @@ function createApiClient(opts) {
36723
36992
  });
36724
36993
  expired.catch(() => {
36725
36994
  });
36726
- return { res, done: () => clearTimeout(timer), expired };
36995
+ return { res, done: () => clearTimeout(timer), expired, timedOut: () => controller.signal.aborted };
36727
36996
  }
36728
36997
  function discard(attempt) {
36729
36998
  attempt.done();
36730
- try {
36731
- const body = attempt.res.body;
36732
- body?.cancel?.()?.catch(() => {
36733
- });
36734
- } catch {
36735
- }
36999
+ cancelBody(attempt.res);
36736
37000
  }
36737
37001
  async function readBody(attempt, read) {
36738
37002
  try {
@@ -36740,15 +37004,40 @@ function createApiClient(opts) {
36740
37004
  return await read(attempt.res);
36741
37005
  return await Promise.race([read(attempt.res), attempt.expired]);
36742
37006
  } catch (err) {
36743
- if (err instanceof RequestTimeoutError) {
36744
- attempt.res.body?.cancel().catch(() => {
36745
- });
37007
+ const timeout = err instanceof RequestTimeoutError || err instanceof Error && err.name === "AbortError" && attempt.timedOut();
37008
+ if (timeout) {
37009
+ cancelBody(attempt.res);
37010
+ throw err instanceof RequestTimeoutError ? err : new RequestTimeoutError(service, timeoutMs);
36746
37011
  }
37012
+ if (!(err instanceof ResponseTooLargeError))
37013
+ markTransportFailure(err);
36747
37014
  throw err;
36748
37015
  } finally {
36749
37016
  attempt.done();
36750
37017
  }
36751
37018
  }
37019
+ function readBytes(attempt, maxBytes) {
37020
+ if (maxBytes === void 0)
37021
+ return readBody(attempt, async (r) => new Uint8Array(await r.arrayBuffer()));
37022
+ const race = (p) => attempt.expired ? Promise.race([p, attempt.expired]) : p;
37023
+ return readBody(attempt, (r) => readBytesCapped(r, maxBytes, service, race));
37024
+ }
37025
+ async function readText(attempt, maxBytes) {
37026
+ if (maxBytes === void 0)
37027
+ return readBody(attempt, (r) => r.text());
37028
+ return new TextDecoder().decode(await readBytes(attempt, maxBytes));
37029
+ }
37030
+ function readErrorText(attempt, maxBytes) {
37031
+ return readText(attempt, maxBytes).catch((err) => {
37032
+ if (err instanceof ResponseTooLargeError)
37033
+ return "";
37034
+ throw err;
37035
+ });
37036
+ }
37037
+ function scanText(attempt, maxBytes) {
37038
+ return readText(attempt, maxBytes).catch(() => "");
37039
+ }
37040
+ const capOf = (opt) => opt.maxBytes !== void 0 ? checkMaxBytes("maxBytes", opt.maxBytes) : defaultMaxBytes;
36752
37041
  const baseParts = (() => {
36753
37042
  try {
36754
37043
  const u = new URL(base);
@@ -36759,6 +37048,14 @@ function createApiClient(opts) {
36759
37048
  })();
36760
37049
  const baseOrigin = baseParts?.origin;
36761
37050
  function resolveUrl(path, query) {
37051
+ const hazard = findPathHazard(path);
37052
+ if (hazard !== void 0) {
37053
+ throw new Error(
37054
+ // Quote only the path part: this fires on hostile input, and the
37055
+ // query may carry values that do not belong in a log line.
37056
+ `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.`
37057
+ );
37058
+ }
36762
37059
  const url2 = `${base}${path}${query}`;
36763
37060
  if (baseOrigin === void 0)
36764
37061
  return url2;
@@ -36773,23 +37070,66 @@ function createApiClient(opts) {
36773
37070
  }
36774
37071
  return url2;
36775
37072
  }
36776
- async function send(method, path, opt) {
37073
+ function sameOriginHop(location, from) {
37074
+ let next;
37075
+ try {
37076
+ next = new URL(location, from);
37077
+ } catch {
37078
+ throw new RedirectRefusedError(`Refusing redirect from ${service}: the Location header is not a valid URL.`);
37079
+ }
37080
+ if (baseOrigin === void 0 || next.origin !== baseOrigin || next.username !== baseParts?.username || next.password !== baseParts?.password) {
37081
+ 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.`);
37082
+ }
37083
+ return next.href;
37084
+ }
37085
+ async function send(method, path, opt, dispatch) {
36777
37086
  const { body: reqBody, contentType } = encodeBody(opt);
36778
37087
  const query = opt.query ? buildQueryString(opt.query) : "";
36779
37088
  const url2 = resolveUrl(path, query);
36780
37089
  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
- });
37090
+ const fetchHop = (hopUrl, hopMethod, withBody, token, signal) => {
37091
+ if (!signal?.aborted)
37092
+ dispatch.sent = true;
37093
+ return doFetch(hopUrl, {
37094
+ method: hopMethod,
37095
+ headers: {
37096
+ Accept: "application/json",
37097
+ ...withBody && contentType !== void 0 ? { "Content-Type": contentType } : {},
37098
+ ...opts.baseHeaders,
37099
+ ...authHeader(token || void 0),
37100
+ ...opt.headers
37101
+ },
37102
+ ...signal ? { signal } : {},
37103
+ ...redirectMode !== void 0 ? { redirect: redirectMode === "same-origin" ? "manual" : redirectMode } : {},
37104
+ ...withBody ? bodyInit : {}
37105
+ }).catch((err) => {
37106
+ markTransportFailure(err);
37107
+ throw err;
37108
+ });
37109
+ };
37110
+ const fetchWith = async (token, signal) => {
37111
+ if (redirectMode !== "same-origin")
37112
+ return fetchHop(url2, method, true, token, signal);
37113
+ let hopUrl = url2;
37114
+ let hopMethod = method;
37115
+ let withBody = true;
37116
+ for (let hops = 0; ; hops += 1) {
37117
+ const res = await fetchHop(hopUrl, hopMethod, withBody, token, signal);
37118
+ const location = REDIRECT_STATUSES.has(res.status) ? responseHeader(res, "location") : void 0;
37119
+ if (location === void 0 || location === null)
37120
+ return res;
37121
+ cancelBody(res);
37122
+ if (hops >= maxRedirects) {
37123
+ throw new RedirectRefusedError(`Refusing request to ${service}: too many redirects (more than ${maxRedirects}).`);
37124
+ }
37125
+ hopUrl = sameOriginHop(location, hopUrl);
37126
+ if (res.status === 303 || (res.status === 301 || res.status === 302) && hopMethod === "POST") {
37127
+ if (hopMethod !== "HEAD")
37128
+ hopMethod = "GET";
37129
+ withBody = false;
37130
+ }
37131
+ }
37132
+ };
36793
37133
  const once = () => withTimeout((signal) => opts.tokenManager ? opts.tokenManager.withAuth((token) => fetchWith(token, signal)) : (async () => fetchWith(await opts.getToken?.() || void 0, signal))());
36794
37134
  let attempt = 0;
36795
37135
  for (; ; ) {
@@ -36808,20 +37148,20 @@ function createApiClient(opts) {
36808
37148
  return current;
36809
37149
  }
36810
37150
  }
36811
- async function unauthorizedOrEdge(attempt, method, path) {
37151
+ async function unauthorizedOrEdge(attempt, method, path, maxBytes) {
36812
37152
  const res = attempt.res;
36813
37153
  let text = "";
36814
37154
  if (/json/i.test(responseHeader(res, "content-type") ?? "")) {
36815
37155
  discard(attempt);
36816
37156
  } else {
36817
- text = await readBody(attempt, (r) => r.text()).catch(() => "");
37157
+ text = await scanText(attempt, maxBytes);
36818
37158
  }
36819
37159
  const edge = detectEdgeBlock({ body: text, headers: res.headers, status: res.status });
36820
37160
  if (edge)
36821
37161
  return new EdgeBlockedError(res.status, edge.vendor, { service, method, path });
36822
37162
  return unauthorized();
36823
37163
  }
36824
- async function rateLimitedError(attempt, method, path) {
37164
+ async function rateLimitedError(attempt, method, path, maxBytes) {
36825
37165
  const res = attempt.res;
36826
37166
  const retryAfter = responseHeader(res, "retry-after");
36827
37167
  const retryAfterMs = retryAfterToMs(retryAfter);
@@ -36833,61 +37173,92 @@ function createApiClient(opts) {
36833
37173
  if (/json/i.test(responseHeader(res, "content-type") ?? "")) {
36834
37174
  discard(attempt);
36835
37175
  } else {
36836
- text = await readBody(attempt, (r) => r.text()).catch(() => "");
37176
+ text = await scanText(attempt, maxBytes);
36837
37177
  }
36838
37178
  const edgeBlock = detectEdgeBlock({ body: text, headers: res.headers, status: res.status });
36839
37179
  return opts.onRateLimited({ status: res.status, retryAfter, retryAfterMs, edgeBlock, method, path });
36840
37180
  }
36841
- async function fetchJson(method, path, opt = {}) {
36842
- const attempt = await send(method, path, opt);
37181
+ async function guardWrite(method, opt, call) {
37182
+ const dispatch = { sent: false };
37183
+ try {
37184
+ return await call(dispatch);
37185
+ } catch (err) {
37186
+ const write = reportWriteOutcome && opt.idempotent !== true && !SAFE_METHODS.has(method.toUpperCase());
37187
+ if (!write || !dispatch.sent)
37188
+ throw err;
37189
+ if (err instanceof RequestTimeoutError) {
37190
+ throw new WriteOutcomeUnknownError(service, method, {
37191
+ timeoutMs: err.timeoutMs,
37192
+ cause: err,
37193
+ hint: opts.writeOutcomeHint
37194
+ });
37195
+ }
37196
+ if (typeof err === "object" && err !== null && transportFailures.has(err)) {
37197
+ throw new WriteOutcomeUnknownError(service, method, { cause: err, hint: opts.writeOutcomeHint });
37198
+ }
37199
+ throw err;
37200
+ }
37201
+ }
37202
+ const fetchJson = (method, path, opt = {}) => guardWrite(method, opt, (dispatch) => fetchJsonOnce(method, path, opt, dispatch));
37203
+ const fetchHtml = (method, path, opt = {}) => guardWrite(method, opt, (dispatch) => fetchHtmlOnce(method, path, opt, dispatch));
37204
+ const fetchRaw = (method, path, opt = {}) => guardWrite(method, opt, (dispatch) => fetchRawOnce(method, path, opt, dispatch));
37205
+ async function fetchJsonOnce(method, path, opt, dispatch) {
37206
+ const maxBytes = capOf(opt);
37207
+ const attempt = await send(method, path, opt, dispatch);
36843
37208
  const res = attempt.res;
36844
37209
  if (res.status === 401)
36845
- throw await unauthorizedOrEdge(attempt, method, path);
37210
+ throw await unauthorizedOrEdge(attempt, method, path, maxBytes);
36846
37211
  if (res.status === 429)
36847
- throw await rateLimitedError(attempt, method, path);
37212
+ throw await rateLimitedError(attempt, method, path, maxBytes);
36848
37213
  if (res.status === 204) {
36849
37214
  discard(attempt);
36850
- return void 0;
37215
+ return parseJsonBody("", { expect: opt.expect, service, method, path, status: 204 });
36851
37216
  }
36852
- const text = await readBody(attempt, (r) => r.text());
36853
37217
  if (!res.ok) {
36854
- throw httpError(res, text, method, path, service);
37218
+ throw httpError(res, await readErrorText(attempt, maxBytes), method, path, service);
36855
37219
  }
36856
- if (text.length === 0)
36857
- return void 0;
36858
- return JSON.parse(text);
37220
+ const text = await readText(attempt, maxBytes);
37221
+ return parseJsonBody(text, {
37222
+ expect: opt.expect,
37223
+ service,
37224
+ method,
37225
+ path,
37226
+ status: res.status,
37227
+ headers: res.headers
37228
+ });
36859
37229
  }
36860
- async function fetchHtml(method, path, opt = {}) {
37230
+ async function fetchHtmlOnce(method, path, opt, dispatch) {
37231
+ const maxBytes = capOf(opt);
36861
37232
  const headers = { Accept: "text/html,*/*", ...opt.headers };
36862
- const attempt = await send(method, path, { ...opt, headers });
37233
+ const attempt = await send(method, path, { ...opt, headers }, dispatch);
36863
37234
  const res = attempt.res;
36864
37235
  if (res.status === 401)
36865
- throw await unauthorizedOrEdge(attempt, method, path);
37236
+ throw await unauthorizedOrEdge(attempt, method, path, maxBytes);
36866
37237
  if (res.status === 429)
36867
- throw await rateLimitedError(attempt, method, path);
36868
- const text = await readBody(attempt, (r) => r.text());
37238
+ throw await rateLimitedError(attempt, method, path, maxBytes);
36869
37239
  if (!res.ok) {
36870
- throw httpError(res, text, method, path, service);
37240
+ throw httpError(res, await readErrorText(attempt, maxBytes), method, path, service);
36871
37241
  }
36872
- return text;
37242
+ return readText(attempt, maxBytes);
36873
37243
  }
36874
- async function fetchRaw(method, path, opt = {}) {
37244
+ async function fetchRawOnce(method, path, opt, dispatch) {
37245
+ const maxBytes = capOf(opt);
36875
37246
  const headers = { Accept: "*/*", ...opt.headers };
36876
- const attempt = await send(method, path, { ...opt, headers });
37247
+ const attempt = await send(method, path, { ...opt, headers }, dispatch);
36877
37248
  const res = attempt.res;
36878
37249
  if (res.status === 401)
36879
- throw await unauthorizedOrEdge(attempt, method, path);
37250
+ throw await unauthorizedOrEdge(attempt, method, path, maxBytes);
36880
37251
  if (res.status === 429)
36881
- throw await rateLimitedError(attempt, method, path);
37252
+ throw await rateLimitedError(attempt, method, path, maxBytes);
36882
37253
  if (!res.ok) {
36883
- const text = await readBody(attempt, (r) => r.text()).catch((err) => {
37254
+ const text = await readText(attempt, maxBytes).catch((err) => {
36884
37255
  if (err instanceof RequestTimeoutError)
36885
37256
  throw err;
36886
37257
  return "";
36887
37258
  });
36888
37259
  throw httpError(res, text, method, path, service);
36889
37260
  }
36890
- const bytes = new Uint8Array(await readBody(attempt, (r) => r.arrayBuffer()));
37261
+ const bytes = await readBytes(attempt, maxBytes);
36891
37262
  return {
36892
37263
  status: res.status,
36893
37264
  contentType: responseHeader(res, "content-type"),
@@ -36897,6 +37268,14 @@ function createApiClient(opts) {
36897
37268
  }
36898
37269
  return { fetchJson, fetchHtml, fetchRaw };
36899
37270
  }
37271
+ function checkMaxBytes(name, value) {
37272
+ if (value === void 0)
37273
+ return void 0;
37274
+ if (typeof value !== "number" || Number.isNaN(value) || value < 0) {
37275
+ throw new TypeError(`createApiClient: ${name} must be a non-negative number, got ${String(value)}.`);
37276
+ }
37277
+ return value === Infinity ? void 0 : value;
37278
+ }
36900
37279
  function buildQueryString(params) {
36901
37280
  const parts = [];
36902
37281
  for (const [key, value] of Object.entries(params)) {
@@ -36947,7 +37326,7 @@ function decodeJwtExp(token) {
36947
37326
  }
36948
37327
  return exp;
36949
37328
  }
36950
- var DEFAULT_RETRY, defaultSleep, UnauthorizedError, RateLimitedError, RequestTimeoutError, ApiError, EdgeBlockedError, EDGE_BLOCK_SCAN_MAX, EDGE_BLOCK_MARKERS;
37329
+ 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
37330
  var init_http = __esm({
36952
37331
  "node_modules/@chrischall/mcp-utils/dist/http/index.js"() {
36953
37332
  init_headers();
@@ -36957,12 +37336,21 @@ var init_http = __esm({
36957
37336
  init_throttle();
36958
37337
  init_response_cache();
36959
37338
  init_net_atoms();
37339
+ init_fetch_bounded();
37340
+ init_url_safety();
37341
+ init_body_limit();
36960
37342
  init_net_atoms();
37343
+ init_url_safety();
36961
37344
  init_retry_after();
36962
37345
  DEFAULT_RETRY = { count: 1, delayMs: 2e3 };
36963
37346
  defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
37347
+ DEFAULT_REQUEST_TIMEOUT_MS = 3e4;
37348
+ DEFAULT_MAX_REDIRECTS = 5;
37349
+ REDIRECT_STATUSES = /* @__PURE__ */ new Set([301, 302, 303, 307, 308]);
36964
37350
  UnauthorizedError = class extends Error {
36965
37351
  status = 401;
37352
+ /** Always `'credential_rejected'` unless a subclass says otherwise. */
37353
+ kind = "credential_rejected";
36966
37354
  constructor(service) {
36967
37355
  super(`Unauthorized (401) from ${service} \u2014 the token is missing, invalid, or expired.`);
36968
37356
  this.name = "UnauthorizedError";
@@ -36971,6 +37359,8 @@ var init_http = __esm({
36971
37359
  };
36972
37360
  RateLimitedError = class extends Error {
36973
37361
  status = 429;
37362
+ /** `'http'`: the far side answered, with a 429 (see `status`). */
37363
+ kind = "http";
36974
37364
  retryAfterMs;
36975
37365
  constructor(service, retryAfterMs) {
36976
37366
  super(`Rate limited (429) by ${service} after retries.`);
@@ -36981,6 +37371,8 @@ var init_http = __esm({
36981
37371
  };
36982
37372
  RequestTimeoutError = class extends Error {
36983
37373
  timeoutMs;
37374
+ /** Always `'timeout'` unless a subclass says otherwise. */
37375
+ kind = "timeout";
36984
37376
  constructor(service, timeoutMs) {
36985
37377
  super(`Request to ${service} timed out after ${timeoutMs}ms.`);
36986
37378
  this.name = "RequestTimeoutError";
@@ -36988,6 +37380,33 @@ var init_http = __esm({
36988
37380
  Object.setPrototypeOf(this, new.target.prototype);
36989
37381
  }
36990
37382
  };
37383
+ DEFAULT_WRITE_OUTCOME_HINT = "The write may have happened \u2014 check before retrying; do not resend blindly.";
37384
+ WriteOutcomeUnknownError = class extends McpToolError {
37385
+ /** Always `true`: whether the write happened is not known. */
37386
+ outcomeUnknown = true;
37387
+ /** Always `false`: never replay this request without checking first. */
37388
+ retrySafe = false;
37389
+ /** `true` when the request's timeout expired; `false` for a dropped connection or broken body. */
37390
+ timedOut;
37391
+ /** The timeout that expired, when {@link timedOut}. */
37392
+ timeoutMs;
37393
+ /** The request method, upper-cased. */
37394
+ method;
37395
+ constructor(service, method, opts = {}) {
37396
+ const verb = method.toUpperCase();
37397
+ const what = opts.timeoutMs !== void 0 ? `${verb} to ${service} timed out after ${opts.timeoutMs}ms` : `${verb} to ${service} failed: ${truncateErrorMessage(messageOf(opts.cause), 200)}`;
37398
+ super(`${what} \u2014 the write may already have been applied (outcome is unknown).`, {
37399
+ hint: opts.hint ?? DEFAULT_WRITE_OUTCOME_HINT,
37400
+ cause: opts.cause,
37401
+ kind: opts.timeoutMs !== void 0 ? "timeout" : "transport"
37402
+ });
37403
+ this.name = "WriteOutcomeUnknownError";
37404
+ this.timedOut = opts.timeoutMs !== void 0;
37405
+ this.timeoutMs = opts.timeoutMs;
37406
+ this.method = verb;
37407
+ }
37408
+ };
37409
+ SAFE_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD", "OPTIONS", "TRACE"]);
36991
37410
  ApiError = class extends Error {
36992
37411
  status;
36993
37412
  constructor(status, message) {
@@ -37000,6 +37419,8 @@ var init_http = __esm({
37000
37419
  EdgeBlockedError = class extends ApiError {
37001
37420
  /** Which edge refused the request, e.g. `'CloudFront'` or `'Cloudflare'`. */
37002
37421
  vendor;
37422
+ /** Always `'edge_blocked'`: the credential was never judged. */
37423
+ kind = "edge_blocked";
37003
37424
  /**
37004
37425
  * @param where The service the request was for, and — when the thrower knows
37005
37426
  * it — the method and path, which the message names.
@@ -37063,6 +37484,7 @@ var init_concurrency = __esm({
37063
37484
  // node_modules/@chrischall/mcp-utils/dist/dates/index.js
37064
37485
  var init_dates = __esm({
37065
37486
  "node_modules/@chrischall/mcp-utils/dist/dates/index.js"() {
37487
+ init_config();
37066
37488
  }
37067
37489
  });
37068
37490
 
@@ -37083,7 +37505,7 @@ var init_zod2 = __esm({
37083
37505
  NonNegInt = external_exports.number().int().nonnegative();
37084
37506
  NonEmptyString = external_exports.string().min(1);
37085
37507
  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");
37508
+ 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
37509
  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
37510
  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
37511
  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 +37540,7 @@ var init_auth = __esm({
37118
37540
  init_config();
37119
37541
  init_errors3();
37120
37542
  init_http();
37543
+ init_cancel();
37121
37544
  init_cached_token();
37122
37545
  init_es256();
37123
37546
  }
@@ -37148,7 +37571,7 @@ var init_version = __esm({
37148
37571
  "src/version.ts"() {
37149
37572
  "use strict";
37150
37573
  PACKAGE_NAME = "office-outlook-mcp";
37151
- VERSION = "0.3.5";
37574
+ VERSION = "0.4.1";
37152
37575
  }
37153
37576
  });
37154
37577
 
@@ -44875,7 +45298,7 @@ function assertUrlInDomains(field, url2, domains) {
44875
45298
  const declared = domains.map((d) => JSON.stringify(d)).join(", ");
44876
45299
  throw new Error(`FetchproxyServer: ${field} host "${host}" is outside declared domains [${declared}] \u2014 must be one of them or a subdomain`);
44877
45300
  }
44878
- function parseJsonBody(response) {
45301
+ function parseJsonBody2(response) {
44879
45302
  if (response.status === 204 || response.body.trim() === "")
44880
45303
  return null;
44881
45304
  return JSON.parse(response.body);
@@ -45944,7 +46367,7 @@ var init_ws_server = __esm({
45944
46367
  */
45945
46368
  async getJson(path, opts = {}) {
45946
46369
  const response = await this.get(path, this.applyJsonDefaults(opts));
45947
- return parseJsonBody(response);
46370
+ return parseJsonBody2(response);
45948
46371
  }
45949
46372
  /**
45950
46373
  * POST a JSON body and parse the response body as JSON. The body is
@@ -45963,7 +46386,7 @@ var init_ws_server = __esm({
45963
46386
  headers,
45964
46387
  body: body === void 0 ? void 0 : JSON.stringify(body)
45965
46388
  });
45966
- return parseJsonBody(response);
46389
+ return parseJsonBody2(response);
45967
46390
  }
45968
46391
  /**
45969
46392
  * GET a path and return the response body as a string. Throws
@@ -46028,7 +46451,7 @@ var init_ws_server = __esm({
46028
46451
  };
46029
46452
  let data;
46030
46453
  try {
46031
- data = parseJsonBody(response);
46454
+ data = parseJsonBody2(response);
46032
46455
  } catch (e) {
46033
46456
  throw new Error(`fetchproxy ${method} ${path} \u2014 response was not JSON: ${e instanceof Error ? e.message : String(e)}`);
46034
46457
  }
@@ -47164,15 +47587,12 @@ var init_dist4 = __esm({
47164
47587
  });
47165
47588
 
47166
47589
  // 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
47590
  function isArm(kind) {
47174
47591
  return kind !== void 0 && CREDENTIAL_ARMS.has(kind);
47175
47592
  }
47593
+ function fromSource(source) {
47594
+ return source !== null ? ` from '${source}'` : "";
47595
+ }
47176
47596
  function credentialHint(arm, prefix, hostLabel, source) {
47177
47597
  switch (arm) {
47178
47598
  case "ok":
@@ -47180,15 +47600,15 @@ function credentialHint(arm, prefix, hostLabel, source) {
47180
47600
  case "no_credential":
47181
47601
  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
47602
  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.`;
47603
+ 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
47604
  case "edge_blocked":
47185
47605
  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
47606
  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.`;
47607
+ 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
47608
  case "verification_pending":
47189
47609
  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
47610
  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.`;
47611
+ 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
47612
  case "http":
47193
47613
  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
47614
  case "transport":
@@ -47209,7 +47629,8 @@ async function runCredentialHealthcheck(args) {
47209
47629
  state = await resolveCredential();
47210
47630
  } catch (e) {
47211
47631
  const edge = edgeBlockOf(e);
47212
- const classified = classifyThrown?.(e) ?? (edge !== null ? { kind: "edge_blocked", detail: { vendor: edge.vendor } } : void 0);
47632
+ const declared = errorKindOf(e);
47633
+ const classified = classifyThrown?.(e) ?? (edge !== null ? { kind: "edge_blocked", detail: { vendor: edge.vendor } } : declared !== void 0 ? { kind: declared } : void 0);
47213
47634
  const result2 = {
47214
47635
  ok: false,
47215
47636
  // Still false, and still no source: a classification explains WHY
@@ -47256,10 +47677,11 @@ async function runCredentialHealthcheck(args) {
47256
47677
  try {
47257
47678
  await probeFn();
47258
47679
  } catch (e) {
47259
- status = statusOf2(e);
47680
+ status = errorStatusOf(e);
47260
47681
  const aborted2 = e instanceof Error && e.name === "AbortError";
47261
47682
  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";
47683
+ const declared = errorKindOf(e);
47684
+ 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
47685
  let kind = arm;
47264
47686
  let detail = edge !== null ? { vendor: edge.vendor } : void 0;
47265
47687
  const custom2 = classifyThrown?.(e);
@@ -47618,6 +48040,7 @@ init_dist2();
47618
48040
  // node_modules/@chrischall/mcp-utils/dist/session/index.js
47619
48041
  init_response();
47620
48042
  init_config();
48043
+ init_errors3();
47621
48044
  init_http();
47622
48045
  init_edge_block();
47623
48046
  import { existsSync as existsSync3, readFileSync as readFileSync3, mkdirSync as mkdirSync4, chmodSync as chmodSync2, renameSync as renameSync2, unlinkSync as unlinkSync4 } from "node:fs";
@@ -48099,7 +48522,9 @@ var TokenManager = class {
48099
48522
  /** Spend `rt`, then install and persist what comes back. */
48100
48523
  async exchange(rt) {
48101
48524
  if (rt === void 0) {
48102
- throw new Error("TokenManager: cannot refresh \u2014 no refresh token is available.");
48525
+ throw new McpToolError("TokenManager: cannot refresh \u2014 no refresh token is available.", {
48526
+ kind: "no_credential"
48527
+ });
48103
48528
  }
48104
48529
  const tok = await this.refreshFn(rt);
48105
48530
  this.tokens = {
@@ -48347,7 +48772,8 @@ var OutlookClient = class {
48347
48772
  * — measured 9.4x smaller on a real message, and it also normalises a mixed
48348
48773
  * HTML/Text collection to one type. `prefer` sets the `Prefer` header
48349
48774
  * directly, for anything else the API takes there (notably
48350
- * `outlook.timezone="…"`).
48775
+ * `outlook.timezone="…"`). `query` is serialised by mcp-utils'
48776
+ * `buildQueryString`, which drops `undefined`/empty values.
48351
48777
  */
48352
48778
  async get(path, opts = {}) {
48353
48779
  this.#requireConfigured();
@@ -48356,7 +48782,8 @@ var OutlookClient = class {
48356
48782
  opts.prefer
48357
48783
  ].filter((p) => p !== void 0);
48358
48784
  return this.#client().fetchJson("GET", path, {
48359
- headers: prefer.length ? { Prefer: prefer.join(", ") } : void 0
48785
+ headers: prefer.length ? { Prefer: prefer.join(", ") } : void 0,
48786
+ ...opts.query ? { query: opts.query } : {}
48360
48787
  });
48361
48788
  }
48362
48789
  /**
@@ -48387,6 +48814,22 @@ var OutlookClient = class {
48387
48814
  }
48388
48815
  return this.get(target.pathname.slice(prefix.length) + target.search, opts);
48389
48816
  }
48817
+ /**
48818
+ * POST a read-only query — `findmeetingtimes`, `getschedule` — that changes
48819
+ * nothing in the mailbox. Kept apart from `write` so a reader can tell the
48820
+ * two apart at the call site, and because these take `Prefer` (notably
48821
+ * `outlook.timezone`, without which every returned slot is UTC).
48822
+ */
48823
+ async post(path, body, opts = {}) {
48824
+ this.#requireConfigured();
48825
+ return this.#client().fetchJson("POST", path, {
48826
+ body,
48827
+ headers: opts.prefer ? { Prefer: opts.prefer } : void 0,
48828
+ // Read-only: a timeout or dropped connection is safe to retry, so keep
48829
+ // the plain error rather than mcp-utils' WriteOutcomeUnknownError.
48830
+ idempotent: true
48831
+ });
48832
+ }
48390
48833
  /**
48391
48834
  * The single write path. Everything mutating goes through here so auth and
48392
48835
  * error shaping stay in one place.
@@ -48429,6 +48872,7 @@ init_dist2();
48429
48872
 
48430
48873
  // src/view.ts
48431
48874
  init_dist2();
48875
+ var VIEWS2 = ["compact", "full", "raw"];
48432
48876
  function addr(r) {
48433
48877
  const e = r?.EmailAddress;
48434
48878
  if (!e) return void 0;
@@ -48487,7 +48931,11 @@ function fullEvent(e) {
48487
48931
  Attendees: e.Attendees?.map(
48488
48932
  (a) => pruned({ Who: addr(a), Response: a.Status?.Response })
48489
48933
  ),
48490
- OnlineMeetingUrl: e.OnlineMeetingUrl,
48934
+ // The Teams link lives under OnlineMeeting; OnlineMeetingUrl is empty on
48935
+ // every live Teams meeting checked.
48936
+ JoinUrl: e.OnlineMeeting?.JoinUrl || e.OnlineMeetingUrl || void 0,
48937
+ // Kept beside JoinUrl so a consumer of the old field still finds it.
48938
+ OnlineMeetingUrl: e.OnlineMeetingUrl || void 0,
48491
48939
  Preview: e.BodyPreview?.trim() || void 0,
48492
48940
  WebLink: e.WebLink
48493
48941
  });
@@ -48497,7 +48945,9 @@ function compactFolder(f) {
48497
48945
  Id: f.Id,
48498
48946
  Name: f.DisplayName,
48499
48947
  Unread: f.UnreadItemCount,
48500
- Total: f.TotalItemCount
48948
+ Total: f.TotalItemCount,
48949
+ // Listings are one level deep; a non-zero count says there is more below.
48950
+ Children: f.ChildFolderCount
48501
48951
  });
48502
48952
  }
48503
48953
  function stripOData(o) {
@@ -48531,7 +48981,9 @@ var nextLinkParam = external_exports.string().min(1).optional().describe(
48531
48981
  "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."
48532
48982
  );
48533
48983
  function fetchPage(client2, nextLink, path, opts = {}) {
48534
- return nextLink !== void 0 ? client2.getAbsolute(nextLink, opts) : client2.get(path, opts);
48984
+ if (nextLink === void 0) return client2.get(path, opts);
48985
+ const { query: _ignored, ...headerOpts } = opts;
48986
+ return client2.getAbsolute(nextLink, headerOpts);
48535
48987
  }
48536
48988
  function plainCollection(data) {
48537
48989
  const items = Array.isArray(data?.value) ? data.value : [];
@@ -48550,7 +49002,6 @@ function mailboxUntrusted(payload) {
48550
49002
  }
48551
49003
 
48552
49004
  // src/tools/mail.ts
48553
- var VIEWS2 = ["compact", "full", "raw"];
48554
49005
  var WELL_KNOWN = [
48555
49006
  "inbox",
48556
49007
  "drafts",
@@ -48562,14 +49013,6 @@ var WELL_KNOWN = [
48562
49013
  "clutter"
48563
49014
  ];
48564
49015
  var LIST_SELECT = "Id,Subject,From,ToRecipients,ReceivedDateTime,IsRead,HasAttachments,BodyPreview";
48565
- function qs(params) {
48566
- const parts = [];
48567
- for (const [k, v] of Object.entries(params)) {
48568
- if (v === void 0 || v === "") continue;
48569
- parts.push(`${k}=${encodeURIComponent(String(v))}`);
48570
- }
48571
- return parts.length ? `?${parts.join("&")}` : "";
48572
- }
48573
49016
  function folderSegment(folder) {
48574
49017
  return encodeURIComponent(folder);
48575
49018
  }
@@ -48577,19 +49020,22 @@ function registerMailTools(server, client2) {
48577
49020
  server.registerTool(
48578
49021
  "outlook_list_folders",
48579
49022
  {
48580
- 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.",
48581
- annotations: { readOnlyHint: true },
49023
+ 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.",
49024
+ annotations: { readOnlyHint: true, openWorldHint: true },
48582
49025
  inputSchema: external_exports.object({
48583
49026
  view: viewParam(VIEWS2),
49027
+ 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."),
48584
49028
  limit: external_exports.number().int().min(1).max(200).optional().describe("Max folders (default 50)"),
48585
49029
  nextLink: nextLinkParam
48586
49030
  })
48587
49031
  },
48588
- async ({ view, limit, nextLink }) => {
49032
+ async ({ view, parent, limit, nextLink }) => {
49033
+ const base = parent !== void 0 ? `/me/mailfolders/${folderSegment(parent)}/childfolders` : "/me/mailfolders";
48589
49034
  const data = await fetchPage(
48590
49035
  client2,
48591
49036
  nextLink,
48592
- `/me/mailfolders${qs({ $top: limit ?? 50 })}`
49037
+ base,
49038
+ { query: { $top: limit ?? 50 } }
48593
49039
  );
48594
49040
  if (resolveView(view, VIEWS2) === "raw") return minifiedResult(data);
48595
49041
  return minifiedResult(projectCollection(data, compactFolder, "mail folder"));
@@ -48599,7 +49045,7 @@ function registerMailTools(server, client2) {
48599
49045
  "outlook_list_messages",
48600
49046
  {
48601
49047
  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,
48602
- annotations: { readOnlyHint: true },
49048
+ annotations: { readOnlyHint: true, openWorldHint: true },
48603
49049
  inputSchema: external_exports.object({
48604
49050
  view: viewParam(VIEWS2),
48605
49051
  folder: external_exports.string().optional().describe('Folder id or well-known name (default "inbox")'),
@@ -48631,7 +49077,8 @@ function registerMailTools(server, client2) {
48631
49077
  const data = await fetchPage(
48632
49078
  client2,
48633
49079
  nextLink,
48634
- `${base}${qs(params)}`
49080
+ base,
49081
+ { query: params }
48635
49082
  );
48636
49083
  const v = resolveView(view, VIEWS2);
48637
49084
  if (v === "raw") return mailboxUntrusted(data);
@@ -48644,7 +49091,7 @@ function registerMailTools(server, client2) {
48644
49091
  "outlook_get_message",
48645
49092
  {
48646
49093
  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,
48647
- annotations: { readOnlyHint: true },
49094
+ annotations: { readOnlyHint: true, openWorldHint: true },
48648
49095
  inputSchema: external_exports.object({
48649
49096
  view: viewParam(VIEWS2),
48650
49097
  id: external_exports.string().min(1).describe("Message Id from outlook_list_messages"),
@@ -48664,7 +49111,7 @@ function registerMailTools(server, client2) {
48664
49111
  "outlook_list_attachments",
48665
49112
  {
48666
49113
  description: "List a message's attachments (name, size, content type). Metadata only \u2014 this deliberately does not download bytes.",
48667
- annotations: { readOnlyHint: true },
49114
+ annotations: { readOnlyHint: true, openWorldHint: true },
48668
49115
  inputSchema: external_exports.object({
48669
49116
  id: external_exports.string().min(1).describe("Message Id"),
48670
49117
  nextLink: nextLinkParam
@@ -48674,9 +49121,8 @@ function registerMailTools(server, client2) {
48674
49121
  const data = await fetchPage(
48675
49122
  client2,
48676
49123
  nextLink,
48677
- `/me/messages/${encodeURIComponent(id)}/attachments${qs({
48678
- $select: "Id,Name,Size,ContentType"
48679
- })}`
49124
+ `/me/messages/${encodeURIComponent(id)}/attachments`,
49125
+ { query: { $select: "Id,Name,Size,ContentType" } }
48680
49126
  );
48681
49127
  return minifiedResult(plainCollection(data));
48682
49128
  }
@@ -48688,8 +49134,9 @@ init_zod();
48688
49134
  init_dist2();
48689
49135
 
48690
49136
  // src/timezone.ts
49137
+ init_dist2();
48691
49138
  var zoneByClient = /* @__PURE__ */ new WeakMap();
48692
- async function mailboxTimeZone(client2) {
49139
+ async function mailboxTimeZone(client2, opts = {}) {
48693
49140
  const cached2 = zoneByClient.get(client2);
48694
49141
  if (cached2) return cached2;
48695
49142
  try {
@@ -48697,27 +49144,44 @@ async function mailboxTimeZone(client2) {
48697
49144
  const zone = settings?.TimeZone?.trim() || void 0;
48698
49145
  if (zone) zoneByClient.set(client2, zone);
48699
49146
  return zone;
48700
- } catch {
48701
- return void 0;
49147
+ } catch (e) {
49148
+ if (!opts.required) return void 0;
49149
+ throw new McpToolError("Could not read the mailbox's time zone, so the event time is ambiguous.", {
49150
+ hint: `Retry, or pass \`timeZone\` explicitly (a Windows zone name such as "Eastern Standard Time"). Cause: ${e instanceof Error ? e.message : String(e)}`
49151
+ });
48702
49152
  }
48703
49153
  }
48704
49154
 
48705
49155
  // src/tools/calendar.ts
48706
- var EVENT_SELECT = "Id,Subject,Start,End,Location,Organizer,IsAllDay,IsCancelled,ShowAs,OnlineMeetingUrl,BodyPreview";
48707
- function qs2(params) {
48708
- const parts = [];
48709
- for (const [k, v] of Object.entries(params)) {
48710
- if (v === void 0 || v === "") continue;
48711
- parts.push(`${k}=${encodeURIComponent(String(v))}`);
49156
+ var EVENT_SELECT = "Id,Subject,Start,End,Location,Organizer,IsAllDay,IsCancelled,ShowAs,OnlineMeetingUrl,OnlineMeeting,BodyPreview";
49157
+ function dateTimeTimeZone(value, zone) {
49158
+ if (/(Z|[+-]\d{2}:?\d{2})$/i.test(value)) {
49159
+ const d = new Date(value);
49160
+ if (!Number.isNaN(d.getTime())) {
49161
+ return { DateTime: d.toISOString().replace(/\.\d{3}Z$/, ""), TimeZone: "UTC" };
49162
+ }
49163
+ }
49164
+ return { DateTime: value, TimeZone: zone };
49165
+ }
49166
+ function assertWindow(start, end) {
49167
+ const a = Date.parse(start);
49168
+ const b = Date.parse(end);
49169
+ if (!Number.isNaN(a) && !Number.isNaN(b) && b <= a) {
49170
+ throw new McpToolError(`The window ends (${end}) before it starts (${start}).`, {
49171
+ hint: "`end` must be later than `start`."
49172
+ });
48712
49173
  }
48713
- return parts.length ? `?${parts.join("&")}` : "";
48714
49174
  }
49175
+ function trimTime(dt) {
49176
+ return dt?.replace(/\.0+$/, "");
49177
+ }
49178
+ var attendeeList = (what) => external_exports.array(external_exports.string().min(3)).describe(`Email addresses of ${what}`);
48715
49179
  function registerCalendarTools(server, client2) {
48716
49180
  server.registerTool(
48717
49181
  "outlook_list_events",
48718
49182
  {
48719
49183
  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,
48720
- annotations: { readOnlyHint: true },
49184
+ annotations: { readOnlyHint: true, openWorldHint: true },
48721
49185
  inputSchema: external_exports.object({
48722
49186
  view: viewParam(VIEWS2),
48723
49187
  start: external_exports.string().min(1).describe("Window start, ISO 8601 (e.g. 2026-09-20T00:00:00Z)"),
@@ -48734,14 +49198,17 @@ function registerCalendarTools(server, client2) {
48734
49198
  const data = await fetchPage(
48735
49199
  client2,
48736
49200
  nextLink,
48737
- `/me/calendarview${qs2({
48738
- startDateTime: start,
48739
- endDateTime: end,
48740
- $select: EVENT_SELECT,
48741
- $orderby: "Start/DateTime",
48742
- $top: limit ?? 50
48743
- })}`,
48744
- zone ? { prefer: `outlook.timezone="${zone}"` } : {}
49201
+ "/me/calendarview",
49202
+ {
49203
+ query: {
49204
+ startDateTime: start,
49205
+ endDateTime: end,
49206
+ $select: EVENT_SELECT,
49207
+ $orderby: "Start/DateTime",
49208
+ $top: limit ?? 50
49209
+ },
49210
+ ...zone ? { prefer: `outlook.timezone="${zone}"` } : {}
49211
+ }
48745
49212
  );
48746
49213
  const v = resolveView(view, VIEWS2);
48747
49214
  if (v === "raw") return mailboxUntrusted(data);
@@ -48754,7 +49221,7 @@ function registerCalendarTools(server, client2) {
48754
49221
  "outlook_get_event",
48755
49222
  {
48756
49223
  description: "Get one calendar event in full, including attendees and their responses. " + UNTRUSTED_DESCRIPTION_SUFFIX,
48757
- annotations: { readOnlyHint: true },
49224
+ annotations: { readOnlyHint: true, openWorldHint: true },
48758
49225
  inputSchema: external_exports.object({
48759
49226
  view: viewParam(VIEWS2),
48760
49227
  id: external_exports.string().min(1).describe("Event Id from outlook_list_events"),
@@ -48776,31 +49243,136 @@ function registerCalendarTools(server, client2) {
48776
49243
  "outlook_list_calendars",
48777
49244
  {
48778
49245
  description: "List the calendars in the mailbox, for picking one to read or write against.",
48779
- annotations: { readOnlyHint: true },
49246
+ annotations: { readOnlyHint: true, openWorldHint: true },
48780
49247
  inputSchema: external_exports.object({
48781
49248
  view: viewParam(VIEWS2),
48782
49249
  nextLink: nextLinkParam
48783
49250
  })
48784
49251
  },
48785
49252
  async ({ view, nextLink }) => {
48786
- const data = await fetchPage(client2, nextLink, `/me/calendars${qs2({ $select: "Id,Name,Color,CanEdit,Owner" })}`);
49253
+ const data = await fetchPage(client2, nextLink, "/me/calendars", { query: { $select: "Id,Name,Color,CanEdit,Owner" } });
48787
49254
  if (resolveView(view, VIEWS2) === "raw") return minifiedResult(data);
48788
49255
  return minifiedResult(plainCollection(data));
48789
49256
  }
48790
49257
  );
49258
+ server.registerTool(
49259
+ "outlook_find_meeting_times",
49260
+ {
49261
+ 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,
49262
+ annotations: { readOnlyHint: true, openWorldHint: true },
49263
+ inputSchema: external_exports.object({
49264
+ view: viewParam(VIEWS2),
49265
+ attendees: attendeeList("required attendees").min(1),
49266
+ optionalAttendees: attendeeList("optional attendees").optional(),
49267
+ start: external_exports.string().min(1).describe("Earliest the meeting may start, e.g. 2026-10-12T09:00:00"),
49268
+ end: external_exports.string().min(1).describe("Latest the meeting may end, e.g. 2026-10-16T17:00:00"),
49269
+ durationMinutes: external_exports.number().int().min(5).max(1440).describe("Meeting length in minutes"),
49270
+ timeZone: external_exports.string().optional().describe('Windows time-zone name, e.g. "Eastern Standard Time". Defaults to the MAILBOX time zone.'),
49271
+ maxCandidates: external_exports.number().int().min(1).max(50).optional().describe("Max suggestions (default 10)"),
49272
+ workingHoursOnly: external_exports.boolean().optional().describe("Only suggest times inside working hours (default true). false searches around the clock."),
49273
+ minimumAttendeePercentage: external_exports.number().min(0).max(100).optional().describe("Lowest confidence (0-100) a slot needs to be suggested. Lower it to see slots where not everyone is free."),
49274
+ isOrganizerOptional: external_exports.boolean().optional().describe("true when the signed-in user need not attend, so their own calendar is ignored")
49275
+ })
49276
+ },
49277
+ async (args) => {
49278
+ assertWindow(args.start, args.end);
49279
+ const zone = args.timeZone ?? await mailboxTimeZone(client2) ?? "UTC";
49280
+ const body = {
49281
+ Attendees: [
49282
+ ...args.attendees.map((Address) => ({ Type: "Required", EmailAddress: { Address } })),
49283
+ ...(args.optionalAttendees ?? []).map((Address) => ({
49284
+ Type: "Optional",
49285
+ EmailAddress: { Address }
49286
+ }))
49287
+ ],
49288
+ TimeConstraint: {
49289
+ ActivityDomain: args.workingHoursOnly === false ? "Unrestricted" : "Work",
49290
+ Timeslots: [
49291
+ { Start: dateTimeTimeZone(args.start, zone), End: dateTimeTimeZone(args.end, zone) }
49292
+ ]
49293
+ },
49294
+ MeetingDuration: `PT${args.durationMinutes}M`,
49295
+ MaxCandidates: args.maxCandidates ?? 10,
49296
+ ReturnSuggestionReasons: true,
49297
+ ...args.minimumAttendeePercentage !== void 0 ? { MinimumAttendeePercentage: args.minimumAttendeePercentage } : {},
49298
+ ...args.isOrganizerOptional !== void 0 ? { IsOrganizerOptional: args.isOrganizerOptional } : {}
49299
+ };
49300
+ const data = await client2.post("/me/findmeetingtimes", body, { prefer: `outlook.timezone="${zone}"` });
49301
+ if (resolveView(args.view, VIEWS2) === "raw") return mailboxUntrusted(data);
49302
+ const suggestions = (data?.MeetingTimeSuggestions ?? []).map((s) => ({
49303
+ Start: trimTime(s.MeetingTimeSlot?.Start?.DateTime),
49304
+ End: trimTime(s.MeetingTimeSlot?.End?.DateTime),
49305
+ Confidence: s.Confidence,
49306
+ Organizer: s.OrganizerAvailability,
49307
+ Attendees: Object.fromEntries(
49308
+ (s.AttendeeAvailability ?? []).map((a) => [
49309
+ a.Attendee?.EmailAddress?.Address ?? a.Attendee?.EmailAddress?.Name ?? "?",
49310
+ a.Availability
49311
+ ])
49312
+ ),
49313
+ Reason: s.SuggestionReason
49314
+ }));
49315
+ return mailboxUntrusted({
49316
+ timeZone: zone,
49317
+ count: suggestions.length,
49318
+ suggestions,
49319
+ // Outlook explains an empty result ("AttendeesUnavailable", ...); an
49320
+ // empty list without it reads as "nobody is ever free".
49321
+ ...data?.EmptySuggestionsReason ? { emptySuggestionsReason: data.EmptySuggestionsReason } : {}
49322
+ });
49323
+ }
49324
+ );
49325
+ server.registerTool(
49326
+ "outlook_get_schedule",
49327
+ {
49328
+ 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,
49329
+ annotations: { readOnlyHint: true, openWorldHint: true },
49330
+ inputSchema: external_exports.object({
49331
+ view: viewParam(VIEWS2),
49332
+ people: attendeeList("people (or rooms) to look up").min(1).max(100),
49333
+ start: external_exports.string().min(1).describe("Window start, e.g. 2026-10-12T09:00:00"),
49334
+ end: external_exports.string().min(1).describe("Window end, e.g. 2026-10-12T17:00:00"),
49335
+ timeZone: external_exports.string().optional().describe('Windows time-zone name, e.g. "Eastern Standard Time". Defaults to the MAILBOX time zone.'),
49336
+ intervalMinutes: external_exports.number().int().min(5).max(1440).optional().describe("Slot size for the raw view's AvailabilityView string (default 30)")
49337
+ })
49338
+ },
49339
+ async ({ view, people, start, end, timeZone, intervalMinutes }) => {
49340
+ assertWindow(start, end);
49341
+ const zone = timeZone ?? await mailboxTimeZone(client2) ?? "UTC";
49342
+ const data = await client2.post(
49343
+ "/me/calendar/getschedule",
49344
+ {
49345
+ Schedules: people,
49346
+ StartTime: dateTimeTimeZone(start, zone),
49347
+ EndTime: dateTimeTimeZone(end, zone),
49348
+ AvailabilityViewInterval: intervalMinutes ?? 30
49349
+ },
49350
+ { prefer: `outlook.timezone="${zone}"` }
49351
+ );
49352
+ if (resolveView(view, VIEWS2) === "raw") return mailboxUntrusted(data);
49353
+ const schedules = (data?.value ?? []).map(
49354
+ (p) => (
49355
+ // An unresolvable address must not read as "free all day".
49356
+ p.Error !== void 0 && p.Error !== null ? { Who: p.ScheduleId, Error: p.Error } : {
49357
+ Who: p.ScheduleId,
49358
+ Busy: (p.ScheduleItems ?? []).map((i) => ({
49359
+ Start: trimTime(i.Start?.DateTime),
49360
+ End: trimTime(i.End?.DateTime),
49361
+ Status: i.Status,
49362
+ ...i.Subject ? { Subject: i.Subject } : {},
49363
+ ...i.Location ? { Location: i.Location } : {}
49364
+ }))
49365
+ }
49366
+ )
49367
+ );
49368
+ return mailboxUntrusted({ timeZone: zone, schedules });
49369
+ }
49370
+ );
48791
49371
  }
48792
49372
 
48793
49373
  // src/tools/directory.ts
48794
49374
  init_zod();
48795
49375
  init_dist2();
48796
- function qs3(params) {
48797
- const parts = [];
48798
- for (const [k, v] of Object.entries(params)) {
48799
- if (v === void 0 || v === "") continue;
48800
- parts.push(`${k}=${encodeURIComponent(String(v))}`);
48801
- }
48802
- return parts.length ? `?${parts.join("&")}` : "";
48803
- }
48804
49376
  function collection(data, raw) {
48805
49377
  if (raw) return data;
48806
49378
  return plainCollection(data);
@@ -48810,7 +49382,7 @@ function registerDirectoryTools(server, client2) {
48810
49382
  "outlook_get_profile",
48811
49383
  {
48812
49384
  description: "Get the signed-in mailbox's identity (email address, display name, alias). The cheapest call that proves the credential works.",
48813
- annotations: { readOnlyHint: true },
49385
+ annotations: { readOnlyHint: true, openWorldHint: true },
48814
49386
  inputSchema: external_exports.object({})
48815
49387
  },
48816
49388
  async () => minifiedResult(stripOData(await client2.get("/me")))
@@ -48819,7 +49391,7 @@ function registerDirectoryTools(server, client2) {
48819
49391
  "outlook_get_mailbox_settings",
48820
49392
  {
48821
49393
  description: "Get mailbox settings: time zone, working hours, language, and automatic-replies (out-of-office) configuration.",
48822
- annotations: { readOnlyHint: true },
49394
+ annotations: { readOnlyHint: true, openWorldHint: true },
48823
49395
  inputSchema: external_exports.object({})
48824
49396
  },
48825
49397
  async () => minifiedResult(stripOData(await client2.get("/me/MailboxSettings")))
@@ -48828,7 +49400,7 @@ function registerDirectoryTools(server, client2) {
48828
49400
  "outlook_list_contacts",
48829
49401
  {
48830
49402
  description: "List saved contacts from the mailbox address book.",
48831
- annotations: { readOnlyHint: true },
49403
+ annotations: { readOnlyHint: true, openWorldHint: true },
48832
49404
  inputSchema: external_exports.object({
48833
49405
  view: viewParam(VIEWS2),
48834
49406
  limit: external_exports.number().int().min(1).max(200).optional().describe("Max contacts (default 50)"),
@@ -48840,13 +49412,16 @@ function registerDirectoryTools(server, client2) {
48840
49412
  const data = await fetchPage(
48841
49413
  client2,
48842
49414
  nextLink,
48843
- `/me/contacts${qs3({
48844
- $top: limit ?? 50,
48845
- $skip: skip,
48846
- // `MobilePhone1`, not `MobilePhone`: the latter is the Graph name and
48847
- // the v2.0 Contact type rejects it outright with a 400.
48848
- $select: "Id,DisplayName,EmailAddresses,CompanyName,JobTitle,MobilePhone1"
48849
- })}`
49415
+ "/me/contacts",
49416
+ {
49417
+ query: {
49418
+ $top: limit ?? 50,
49419
+ $skip: skip,
49420
+ // `MobilePhone1`, not `MobilePhone`: the latter is the Graph name and
49421
+ // the v2.0 Contact type rejects it outright with a 400.
49422
+ $select: "Id,DisplayName,EmailAddresses,CompanyName,JobTitle,MobilePhone1"
49423
+ }
49424
+ }
48850
49425
  );
48851
49426
  return minifiedResult(collection(data, resolveView(view, VIEWS2) === "raw"));
48852
49427
  }
@@ -48855,7 +49430,7 @@ function registerDirectoryTools(server, client2) {
48855
49430
  "outlook_list_people",
48856
49431
  {
48857
49432
  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.',
48858
- annotations: { readOnlyHint: true },
49433
+ annotations: { readOnlyHint: true, openWorldHint: true },
48859
49434
  inputSchema: external_exports.object({
48860
49435
  view: viewParam(VIEWS2),
48861
49436
  limit: external_exports.number().int().min(1).max(100).optional().describe("Max people (default 25)"),
@@ -48866,10 +49441,13 @@ function registerDirectoryTools(server, client2) {
48866
49441
  const data = await fetchPage(
48867
49442
  client2,
48868
49443
  nextLink,
48869
- `/me/people${qs3({
48870
- $top: limit ?? 25,
48871
- $select: "Id,DisplayName,ScoredEmailAddresses,JobTitle,CompanyName"
48872
- })}`
49444
+ "/me/people",
49445
+ {
49446
+ query: {
49447
+ $top: limit ?? 25,
49448
+ $select: "Id,DisplayName,ScoredEmailAddresses,JobTitle,CompanyName"
49449
+ }
49450
+ }
48873
49451
  );
48874
49452
  return minifiedResult(collection(data, resolveView(view, VIEWS2) === "raw"));
48875
49453
  }
@@ -48878,7 +49456,7 @@ function registerDirectoryTools(server, client2) {
48878
49456
  "outlook_list_tasks",
48879
49457
  {
48880
49458
  description: "List Outlook tasks with status and due dates.",
48881
- annotations: { readOnlyHint: true },
49459
+ annotations: { readOnlyHint: true, openWorldHint: true },
48882
49460
  inputSchema: external_exports.object({
48883
49461
  view: viewParam(VIEWS2),
48884
49462
  limit: external_exports.number().int().min(1).max(200).optional().describe("Max tasks (default 50)"),
@@ -48889,10 +49467,13 @@ function registerDirectoryTools(server, client2) {
48889
49467
  const data = await fetchPage(
48890
49468
  client2,
48891
49469
  nextLink,
48892
- `/me/tasks${qs3({
48893
- $top: limit ?? 50,
48894
- $select: "Id,Subject,Status,Importance,DueDateTime,CompletedDateTime"
48895
- })}`
49470
+ "/me/tasks",
49471
+ {
49472
+ query: {
49473
+ $top: limit ?? 50,
49474
+ $select: "Id,Subject,Status,Importance,DueDateTime,CompletedDateTime"
49475
+ }
49476
+ }
48896
49477
  );
48897
49478
  return minifiedResult(collection(data, resolveView(view, VIEWS2) === "raw"));
48898
49479
  }
@@ -48912,12 +49493,52 @@ function messageBody(subject, body, html) {
48912
49493
  Body: { ContentType: html === true ? "HTML" : "Text", Content: body }
48913
49494
  };
48914
49495
  }
49496
+ var TEAMS = { IsOnlineMeeting: true, OnlineMeetingProvider: "TeamsForBusiness" };
49497
+ var teamsMeetingParam = external_exports.boolean().optional().describe("Attach a Microsoft Teams meeting (default true). false leaves it off.");
49498
+ var EVENT_STATE_SELECT = "Id,Subject,IsOrganizer,Start,End,Location,Attendees,IsOnlineMeeting,OnlineMeetingProvider,OnlineMeeting";
49499
+ function attendee(Address, Type) {
49500
+ return { Type, EmailAddress: { Address } };
49501
+ }
49502
+ var lower = (a) => a.EmailAddress?.Address?.toLowerCase();
49503
+ function joinUrlOf(e) {
49504
+ return e?.OnlineMeeting?.JoinUrl || void 0;
49505
+ }
49506
+ function isTeams(e) {
49507
+ return e.IsOnlineMeeting === true && e.OnlineMeetingProvider === "TeamsForBusiness";
49508
+ }
49509
+ function dateTimeTimeZone2(value, zone) {
49510
+ if (/(Z|[+-]\d{2}:?\d{2})$/i.test(value)) {
49511
+ const d = new Date(value);
49512
+ if (!Number.isNaN(d.getTime())) {
49513
+ return { DateTime: d.toISOString().replace(/\.\d{3}Z$/, ""), TimeZone: "UTC" };
49514
+ }
49515
+ }
49516
+ return { DateTime: value, TimeZone: zone };
49517
+ }
49518
+ 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.';
49519
+ async function writeOrUnknown(run, checkWhere) {
49520
+ try {
49521
+ return { ok: true, value: await run() };
49522
+ } catch (e) {
49523
+ if (!(e instanceof WriteOutcomeUnknownError)) throw e;
49524
+ return {
49525
+ ok: false,
49526
+ result: minifiedResult({
49527
+ status: "unknown",
49528
+ 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.`
49529
+ })
49530
+ };
49531
+ }
49532
+ }
49533
+ var naiveMs = (dt) => Date.parse(`${dt.replace(/\.\d+$/, "")}Z`);
49534
+ var fromNaiveMs = (ms) => new Date(ms).toISOString().replace(/\.\d{3}Z$/, "");
49535
+ var trimTime2 = (dt) => dt?.replace(/\.\d+$/, "");
48915
49536
  function registerWriteTools(server, client2) {
48916
49537
  server.registerTool(
48917
49538
  "outlook_send_mail",
48918
49539
  {
48919
- description: "Send an email from the signed-in mailbox. " + CONFIRM_FLOW_SENTENCE + " The preview shows exactly what would be sent. " + CONFIRM_INJECTION_RULE,
48920
- annotations: { readOnlyHint: false, destructiveHint: true },
49540
+ 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,
49541
+ annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
48921
49542
  inputSchema: external_exports.object({
48922
49543
  to: recipientList,
48923
49544
  cc: recipientList,
@@ -48951,7 +49572,8 @@ function registerWriteTools(server, client2) {
48951
49572
  confirmToken
48952
49573
  });
48953
49574
  if (gate) return gate;
48954
- await client2.write("POST", "/me/sendmail", payload);
49575
+ const sent = await writeOrUnknown(() => client2.write("POST", "/me/sendmail", payload), "Sent Items");
49576
+ if (!sent.ok) return sent.result;
48955
49577
  return minifiedResult({ sent: true, subject, recipients: recipients || null });
48956
49578
  }
48957
49579
  );
@@ -48959,7 +49581,7 @@ function registerWriteTools(server, client2) {
48959
49581
  "outlook_create_draft",
48960
49582
  {
48961
49583
  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,
48962
- annotations: { readOnlyHint: false, destructiveHint: false },
49584
+ annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
48963
49585
  inputSchema: external_exports.object({
48964
49586
  to: recipientList,
48965
49587
  cc: recipientList,
@@ -48998,7 +49620,7 @@ function registerWriteTools(server, client2) {
48998
49620
  "outlook_mark_read",
48999
49621
  {
49000
49622
  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,
49001
- annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true },
49623
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
49002
49624
  inputSchema: external_exports.object({
49003
49625
  id: external_exports.string().min(1).describe("Message Id"),
49004
49626
  isRead: external_exports.boolean().describe("true to mark read, false to mark unread"),
@@ -49033,7 +49655,7 @@ function registerWriteTools(server, client2) {
49033
49655
  "outlook_move_message",
49034
49656
  {
49035
49657
  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,
49036
- annotations: { readOnlyHint: false, destructiveHint: false },
49658
+ annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
49037
49659
  inputSchema: external_exports.object({
49038
49660
  id: external_exports.string().min(1).describe("Message Id"),
49039
49661
  destination: external_exports.string().min(1).describe('Destination folder id or well-known name (e.g. "archive")'),
@@ -49071,8 +49693,8 @@ function registerWriteTools(server, client2) {
49071
49693
  server.registerTool(
49072
49694
  "outlook_create_event",
49073
49695
  {
49074
- description: 'Create a calendar event. `timeZone` takes a WINDOWS zone name such as "Eastern Standard Time", not an IANA name. Attendees are emailed an invitation. ' + CONFIRM_FLOW_SENTENCE + " " + CONFIRM_INJECTION_RULE,
49075
- annotations: { readOnlyHint: false, destructiveHint: false },
49696
+ 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 + " " + TIMEOUT_SENTENCE + " " + CONFIRM_INJECTION_RULE,
49697
+ annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
49076
49698
  inputSchema: external_exports.object({
49077
49699
  subject: external_exports.string().describe("Event title"),
49078
49700
  start: external_exports.string().min(1).describe("Start, ISO 8601 local time e.g. 2026-09-22T15:00:00"),
@@ -49080,42 +49702,176 @@ function registerWriteTools(server, client2) {
49080
49702
  timeZone: external_exports.string().optional().describe("Windows time-zone name. Defaults to the MAILBOX time zone."),
49081
49703
  location: external_exports.string().optional().describe("Location display name"),
49082
49704
  body: external_exports.string().optional().describe("Event description"),
49083
- attendees: recipientList,
49705
+ attendees: recipientList.describe("Required attendees' email addresses"),
49706
+ optionalAttendees: recipientList.describe("Optional attendees' email addresses"),
49707
+ teamsMeeting: teamsMeetingParam,
49084
49708
  confirmToken: confirmTokenParam
49085
49709
  })
49086
49710
  },
49087
- async ({ subject, start, end, timeZone, location, body, attendees, confirmToken }, ctx) => {
49088
- const tz = timeZone ?? await mailboxTimeZone(client2) ?? "UTC";
49711
+ async ({ subject, start, end, timeZone, location, body, attendees, optionalAttendees, teamsMeeting, confirmToken }, ctx) => {
49712
+ const wantTeams = teamsMeeting !== false;
49713
+ const allAttendees = [
49714
+ ...(attendees ?? []).map((a) => attendee(a, "Required")),
49715
+ ...(optionalAttendees ?? []).map((a) => attendee(a, "Optional"))
49716
+ ];
49717
+ const tz = timeZone ?? await mailboxTimeZone(client2, { required: true }) ?? "UTC";
49089
49718
  const payload = {
49090
49719
  Subject: subject,
49091
49720
  Start: { DateTime: start, TimeZone: tz },
49092
49721
  End: { DateTime: end, TimeZone: tz },
49093
49722
  ...location ? { Location: { DisplayName: location } } : {},
49094
49723
  ...body ? { Body: { ContentType: "Text", Content: body } } : {},
49095
- ...attendees?.length ? {
49096
- Attendees: attendees.map((Address) => ({
49097
- EmailAddress: { Address },
49098
- Type: "Required"
49099
- }))
49100
- } : {}
49724
+ ...allAttendees.length ? { Attendees: allAttendees } : {},
49725
+ ...wantTeams ? TEAMS : {}
49101
49726
  };
49102
49727
  const gate = await confirmWrite(ctx, {
49103
49728
  tool: "outlook_create_event",
49104
49729
  action: "calendar.create_event",
49105
49730
  message: "Review and confirm this event (attendees are emailed an invitation):",
49106
- summary: `Create event "${subject}" ${start} to ${end} (${tz})`,
49731
+ summary: `Create ${wantTeams ? "Teams meeting" : "event"} "${subject}" ${start} to ${end} (${tz})`,
49107
49732
  // One signed-in mailbox per server process.
49108
49733
  account: void 0,
49109
49734
  request: { method: "POST", path: "/me/events", body: payload },
49110
49735
  confirmToken
49111
49736
  });
49112
49737
  if (gate) return gate;
49113
- const created = await client2.write(
49114
- "POST",
49115
- "/me/events",
49116
- payload
49738
+ const write = await writeOrUnknown(
49739
+ () => client2.write("POST", "/me/events", payload),
49740
+ "the calendar (outlook_list_events)"
49117
49741
  );
49118
- return minifiedResult({ created: true, Id: created?.Id, WebLink: created?.WebLink });
49742
+ if (!write.ok) return write.result;
49743
+ const created = write.value;
49744
+ let joinUrl = joinUrlOf(created);
49745
+ if (wantTeams && !joinUrl && created?.Id) {
49746
+ const after = await client2.get(
49747
+ `/me/events/${encodeURIComponent(created.Id)}?$select=OnlineMeeting`
49748
+ );
49749
+ joinUrl = joinUrlOf(after);
49750
+ }
49751
+ return minifiedResult({
49752
+ created: true,
49753
+ Id: created?.Id,
49754
+ WebLink: created?.WebLink,
49755
+ TimeZone: tz,
49756
+ ...joinUrl ? { JoinUrl: joinUrl } : {},
49757
+ ...wantTeams && !joinUrl ? { warning: "The event was created, but Outlook returned no Teams join link." } : {}
49758
+ });
49759
+ }
49760
+ );
49761
+ server.registerTool(
49762
+ "outlook_update_event",
49763
+ {
49764
+ 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,
49765
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
49766
+ inputSchema: external_exports.object({
49767
+ id: external_exports.string().min(1).describe("Event Id from outlook_list_events or outlook_get_event"),
49768
+ subject: external_exports.string().optional().describe("New title"),
49769
+ start: external_exports.string().optional().describe("New start, ISO 8601 local time e.g. 2026-09-22T15:00:00"),
49770
+ end: external_exports.string().optional().describe("New end. Omit when moving `start` to keep the current length."),
49771
+ timeZone: external_exports.string().optional().describe("Windows time-zone name for start/end. Defaults to the MAILBOX time zone."),
49772
+ location: external_exports.string().optional().describe("New location display name"),
49773
+ body: external_exports.string().optional().describe("New description (replaces the existing one)"),
49774
+ addAttendees: recipientList.describe("Required attendees to add"),
49775
+ addOptionalAttendees: recipientList.describe("Optional attendees to add"),
49776
+ removeAttendees: recipientList.describe("Attendees to remove"),
49777
+ teamsMeeting: teamsMeetingParam,
49778
+ confirmToken: confirmTokenParam
49779
+ })
49780
+ },
49781
+ async (args, ctx) => {
49782
+ const tz = args.timeZone ?? await mailboxTimeZone(client2, { required: true }) ?? "UTC";
49783
+ const path = `/me/events/${encodeURIComponent(args.id)}`;
49784
+ const readEvent = (zone) => client2.get(`${path}?$select=${EVENT_STATE_SELECT}`, {
49785
+ prefer: `outlook.timezone="${zone}"`
49786
+ });
49787
+ const current = await readEvent(tz);
49788
+ if (current?.IsOrganizer === false) {
49789
+ throw new McpToolError("Only the organizer can update this meeting.", {
49790
+ hint: "You are an attendee. Changing your copy would not reach the organizer or anyone else; ask the organizer, or propose a new time from Outlook."
49791
+ });
49792
+ }
49793
+ const payload = {};
49794
+ if (args.subject !== void 0) payload.Subject = args.subject;
49795
+ if (args.location !== void 0) payload.Location = { DisplayName: args.location };
49796
+ if (args.body !== void 0) payload.Body = { ContentType: "Text", Content: args.body };
49797
+ if (args.start !== void 0 || args.end !== void 0) {
49798
+ const start = args.start !== void 0 ? dateTimeTimeZone2(args.start, tz) : void 0;
49799
+ let end = args.end !== void 0 ? dateTimeTimeZone2(args.end, tz) : void 0;
49800
+ if (start && !end && current?.Start?.DateTime && current?.End?.DateTime) {
49801
+ const length = naiveMs(current.End.DateTime) - naiveMs(current.Start.DateTime);
49802
+ end = { DateTime: fromNaiveMs(naiveMs(start.DateTime) + length), TimeZone: start.TimeZone };
49803
+ }
49804
+ if (args.start !== void 0 && args.end !== void 0 && start?.TimeZone !== end?.TimeZone) {
49805
+ throw new McpToolError("`start` and `end` are in different zones.", {
49806
+ hint: "Give both the same way: both local wall-clock times, or both with Z or an offset."
49807
+ });
49808
+ }
49809
+ const startIn = start ?? (end && end.TimeZone !== tz ? (await readEvent(end.TimeZone))?.Start : current?.Start);
49810
+ if (end && startIn?.DateTime && naiveMs(end.DateTime) <= naiveMs(startIn.DateTime)) {
49811
+ throw new McpToolError("The meeting would end before it starts.", {
49812
+ hint: "`end` must be later than `start`."
49813
+ });
49814
+ }
49815
+ if (start) payload.Start = start;
49816
+ if (end) payload.End = end;
49817
+ }
49818
+ const adding = [
49819
+ ...(args.addAttendees ?? []).map((a) => attendee(a, "Required")),
49820
+ ...(args.addOptionalAttendees ?? []).map((a) => attendee(a, "Optional"))
49821
+ ];
49822
+ const removing = new Set((args.removeAttendees ?? []).map((a) => a.toLowerCase()));
49823
+ if (adding.length || removing.size) {
49824
+ const next = (current?.Attendees ?? []).filter((a) => !removing.has(lower(a) ?? "")).map(({ Type, EmailAddress }) => ({ Type, EmailAddress }));
49825
+ for (const a of adding) {
49826
+ if (!next.some((n) => lower(n) === lower(a))) next.push(a);
49827
+ }
49828
+ payload.Attendees = next;
49829
+ }
49830
+ const addTeams = args.teamsMeeting !== false && !(current && isTeams(current));
49831
+ if (addTeams) Object.assign(payload, TEAMS);
49832
+ if (Object.keys(payload).length === 0) {
49833
+ throw new McpToolError("Nothing to update.", {
49834
+ hint: "Pass at least one of subject, start, end, location, body, or attendee changes."
49835
+ });
49836
+ }
49837
+ const gate = await confirmWrite(ctx, {
49838
+ tool: "outlook_update_event",
49839
+ action: "calendar.update_event",
49840
+ message: "Review and confirm this change (attendees are sent an updated invitation):",
49841
+ summary: `Update "${current?.Subject ?? args.id}": ${Object.keys(payload).join(", ")}`,
49842
+ // One signed-in mailbox per server process.
49843
+ account: void 0,
49844
+ target: args.id,
49845
+ request: { method: "PATCH", path, body: payload },
49846
+ confirmToken: args.confirmToken
49847
+ });
49848
+ if (gate) return gate;
49849
+ await client2.write("PATCH", path, payload);
49850
+ const writtenZone = (payload.Start ?? payload.End)?.TimeZone ?? tz;
49851
+ const after = await readEvent(writtenZone);
49852
+ const unchanged = [];
49853
+ if (payload.Subject !== void 0 && after?.Subject !== payload.Subject) unchanged.push("Subject");
49854
+ for (const key of ["Start", "End"]) {
49855
+ const want = payload[key];
49856
+ if (want && trimTime2(after?.[key]?.DateTime) !== trimTime2(want.DateTime)) unchanged.push(key);
49857
+ }
49858
+ if (payload.Location !== void 0 && after?.Location?.DisplayName !== args.location) {
49859
+ unchanged.push("Location");
49860
+ }
49861
+ if (payload.Attendees !== void 0) {
49862
+ const want = payload.Attendees.map(lower).sort().join();
49863
+ const got = (after?.Attendees ?? []).map(lower).sort().join();
49864
+ if (want !== got) unchanged.push("Attendees");
49865
+ }
49866
+ const joinUrl = joinUrlOf(after);
49867
+ if (addTeams && !joinUrl) unchanged.push("Teams meeting");
49868
+ return minifiedResult({
49869
+ updated: unchanged.length === 0,
49870
+ Id: after?.Id ?? args.id,
49871
+ changed: Object.keys(payload),
49872
+ ...joinUrl ? { JoinUrl: joinUrl } : {},
49873
+ ...unchanged.length ? { warning: `Outlook accepted the update but these did not change: ${unchanged.join(", ")}.` } : {}
49874
+ });
49119
49875
  }
49120
49876
  );
49121
49877
  }