@chrischall/office-outlook-mcp 0.9.0 → 0.10.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.
@@ -7,7 +7,7 @@
7
7
  },
8
8
  "metadata": {
9
9
  "description": "MCP server for Outlook / Microsoft 365 — mail, calendar, contacts and tasks via natural language",
10
- "version": "0.9.0"
10
+ "version": "0.10.1"
11
11
  },
12
12
  "plugins": [
13
13
  {
@@ -15,7 +15,7 @@
15
15
  "displayName": "Outlook (Microsoft 365)",
16
16
  "source": "./",
17
17
  "description": "MCP server for Outlook / Microsoft 365 — triage unread mail, reply, answer meeting invites and schedule meetings, plus folders, calendar, contacts and tasks, with confirmation before anything is sent",
18
- "version": "0.9.0",
18
+ "version": "0.10.1",
19
19
  "author": {
20
20
  "name": "Chris Hall"
21
21
  },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "office-outlook-mcp",
3
3
  "displayName": "Outlook (Microsoft 365)",
4
- "version": "0.9.0",
4
+ "version": "0.10.1",
5
5
  "description": "Outlook / Microsoft 365 for Claude — triage unread mail, reply, answer meeting invites and schedule meetings, plus folders, calendar, contacts and tasks, with confirmation before anything is sent, using your signed-in browser session",
6
6
  "author": {
7
7
  "name": "Chris Hall",
package/dist/bundle.js CHANGED
@@ -39293,7 +39293,7 @@ var init_version = __esm({
39293
39293
  "src/version.ts"() {
39294
39294
  "use strict";
39295
39295
  PACKAGE_NAME = "office-outlook-mcp";
39296
- VERSION = "0.9.0";
39296
+ VERSION = "0.10.1";
39297
39297
  }
39298
39298
  });
39299
39299
 
@@ -39673,6 +39673,38 @@ var init_capability_availability = __esm({
39673
39673
  }
39674
39674
  });
39675
39675
 
39676
+ // node_modules/@fetchproxy/protocol/dist/binary-fetch.js
39677
+ function parseResponseTooLarge(error62) {
39678
+ const declared = TOO_LARGE_DECLARED_RE.exec(error62);
39679
+ if (declared)
39680
+ return { limitBytes: Number(declared[2]), declaredBytes: Number(declared[1]) };
39681
+ const streamed = TOO_LARGE_STREAMED_RE.exec(error62);
39682
+ if (streamed)
39683
+ return { limitBytes: Number(streamed[1]), declaredBytes: null };
39684
+ return null;
39685
+ }
39686
+ function isBase64Body(s) {
39687
+ return s.length % 4 === 0 && BASE64_RE.test(s);
39688
+ }
39689
+ var FETCH_RESPONSE_TYPE_BASE64, EXTENSION_FEATURE_FETCH_BASE64, KNOWN_EXTENSION_FEATURES, MAX_EXTENSION_FEATURES, MAX_EXTENSION_FEATURE_LENGTH, HOSTED_RELAY_MESSAGE_BYTES, MAX_BINARY_RESPONSE_BYTES, RESPONSE_TOO_LARGE_CODE, TOO_LARGE_DECLARED_RE, TOO_LARGE_STREAMED_RE, BASE64_RE;
39690
+ var init_binary_fetch = __esm({
39691
+ "node_modules/@fetchproxy/protocol/dist/binary-fetch.js"() {
39692
+ FETCH_RESPONSE_TYPE_BASE64 = "base64";
39693
+ EXTENSION_FEATURE_FETCH_BASE64 = "fetch_base64";
39694
+ KNOWN_EXTENSION_FEATURES = /* @__PURE__ */ new Set([
39695
+ EXTENSION_FEATURE_FETCH_BASE64
39696
+ ]);
39697
+ MAX_EXTENSION_FEATURES = 32;
39698
+ MAX_EXTENSION_FEATURE_LENGTH = 64;
39699
+ HOSTED_RELAY_MESSAGE_BYTES = 32 * 1024 * 1024;
39700
+ MAX_BINARY_RESPONSE_BYTES = 16 * 1024 * 1024;
39701
+ RESPONSE_TOO_LARGE_CODE = "response_too_large";
39702
+ TOO_LARGE_DECLARED_RE = /^response body too large: content-length (\d{1,16}) exceeds the (\d{1,16})-byte binary cap/;
39703
+ TOO_LARGE_STREAMED_RE = /^response body too large: more than the (\d{1,16})-byte binary cap was received/;
39704
+ BASE64_RE = /^[A-Za-z0-9+/]*={0,2}$/;
39705
+ }
39706
+ });
39707
+
39676
39708
  // node_modules/@fetchproxy/protocol/dist/json-pointer.js
39677
39709
  function matchesDeclaredKey(requested, declared) {
39678
39710
  for (const decl of declared) {
@@ -39884,7 +39916,7 @@ function assertString(x, label) {
39884
39916
  }
39885
39917
  function assertBase64(x, label) {
39886
39918
  assertString(x, label);
39887
- if (!BASE64_RE.test(x))
39919
+ if (!BASE64_RE2.test(x))
39888
39920
  throw new ProtocolError3(`${label}: invalid base64`);
39889
39921
  }
39890
39922
  function base64ByteLength(s) {
@@ -40288,6 +40320,17 @@ function validateFrame(raw) {
40288
40320
  }
40289
40321
  throw new ProtocolError3(`unknown frame type: ${String(t)}`);
40290
40322
  }
40323
+ function assertTabDomains(value, domains, label) {
40324
+ if (!Array.isArray(value))
40325
+ throw new ProtocolError3(`${label}: expected array`);
40326
+ if (value.length === 0)
40327
+ throw new ProtocolError3(`${label}: must be non-empty`);
40328
+ for (const d of value) {
40329
+ if (typeof d !== "string" || !domains.includes(d)) {
40330
+ throw new ProtocolError3(`${label}: ${JSON.stringify(d)} is not one of the declared domains [${domains.join(", ")}]`);
40331
+ }
40332
+ }
40333
+ }
40291
40334
  function validateHello(raw) {
40292
40335
  if (raw.protocolVersion !== PROTOCOL_VERSION) {
40293
40336
  throw new ProtocolError3(`hello.protocolVersion: must be ${PROTOCOL_VERSION}`);
@@ -40364,6 +40407,9 @@ function validateHello(raw) {
40364
40407
  }
40365
40408
  if (raw.accepts !== void 0)
40366
40409
  assertAcceptsList(raw.accepts);
40410
+ if (raw.tabDomains !== void 0) {
40411
+ assertTabDomains(raw.tabDomains, raw.domains, "hello.tabDomains");
40412
+ }
40367
40413
  if (raw.graphqlOps !== void 0) {
40368
40414
  assertGraphqlOpsArray(raw.graphqlOps, "hello.graphqlOps");
40369
40415
  }
@@ -40390,6 +40436,8 @@ function validateHello(raw) {
40390
40436
  if (raw.unavailableCapabilities !== void 0) {
40391
40437
  assertUnavailableCapabilities(raw.unavailableCapabilities);
40392
40438
  }
40439
+ if (raw.features !== void 0)
40440
+ assertExtensionFeatures(raw.features);
40393
40441
  return raw;
40394
40442
  }
40395
40443
  throw new ProtocolError3(`hello.role: must be 'server' or 'extension', got ${String(role)}`);
@@ -40454,6 +40502,22 @@ function assertAcceptsList(accepts) {
40454
40502
  }
40455
40503
  }
40456
40504
  }
40505
+ function assertExtensionFeatures(list) {
40506
+ const label = "hello.features";
40507
+ if (!Array.isArray(list))
40508
+ throw new ProtocolError3(`${label}: expected array`);
40509
+ if (list.length > MAX_EXTENSION_FEATURES) {
40510
+ throw new ProtocolError3(`${label}: at most ${MAX_EXTENSION_FEATURES} entries, got ${list.length}`);
40511
+ }
40512
+ for (const f of list) {
40513
+ if (typeof f !== "string") {
40514
+ throw new ProtocolError3(`${label}: entry must be string, got ${typeof f}`);
40515
+ }
40516
+ if (f.length < 1 || f.length > MAX_EXTENSION_FEATURE_LENGTH) {
40517
+ throw new ProtocolError3(`${label}: entry must be 1-${MAX_EXTENSION_FEATURE_LENGTH} characters, got ${f.length}`);
40518
+ }
40519
+ }
40520
+ }
40457
40521
  function assertUnavailableCapabilities(list) {
40458
40522
  const label = "hello.unavailableCapabilities";
40459
40523
  if (!Array.isArray(list))
@@ -40657,6 +40721,11 @@ function validateInnerRequest(raw) {
40657
40721
  throw new ProtocolError3("inner.init.credentials: must be 'include' or 'omit'");
40658
40722
  }
40659
40723
  }
40724
+ if (raw.init.responseType !== void 0) {
40725
+ if (raw.init.responseType !== FETCH_RESPONSE_TYPE_BASE64) {
40726
+ throw new ProtocolError3("inner.init.responseType: must be 'base64' when present");
40727
+ }
40728
+ }
40660
40729
  return raw;
40661
40730
  }
40662
40731
  if (raw.op === "read_cookies") {
@@ -40961,6 +41030,16 @@ function validateInnerResponse(raw) {
40961
41030
  assertPositiveInt(raw.status, "inner.status");
40962
41031
  assertString(raw.url, "inner.url");
40963
41032
  assertString(raw.body, "inner.body");
41033
+ if (raw.bodyEncoding !== void 0) {
41034
+ if (raw.bodyEncoding !== "base64") {
41035
+ throw new ProtocolError3("inner.bodyEncoding: must be 'base64' when present");
41036
+ }
41037
+ if (!isBase64Body(raw.body)) {
41038
+ throw new ProtocolError3("inner.body: not standard padded base64 under bodyEncoding base64");
41039
+ }
41040
+ }
41041
+ if (raw.headers !== void 0)
41042
+ assertStringMap(raw.headers, "inner.headers");
40964
41043
  return raw;
40965
41044
  }
40966
41045
  if (op === "read_cookies") {
@@ -41079,11 +41158,12 @@ function validateInnerResponse(raw) {
41079
41158
  }
41080
41159
  throw new ProtocolError3(`inner.ok: must be boolean, got ${typeof raw.ok}`);
41081
41160
  }
41082
- var ProtocolError3, FORBIDDEN_KEYS, BASE64_RE, SCOPE_KEY_RE, SCOPE_KEY_GLOB_RE, HEADER_NAME_RE, HOSTNAME_RE, CAPTURE_PATH_RE, DOM_SELECTOR_RE, DOM_ATTRIBUTE_RE, GRAPHQL_OP_NAME_RE, PAIR_CODE_RE, DISPLAY_FORBIDDEN_RE, ACCOUNT_KEY_FIELDS, ACCOUNT_ATTEST_FIELDS, TYPE_ONLY_FIELDS, BRIDGE_ROLE_FIELDS, BRIDGE_ROLE_SERVING_FIELDS, KNOWN_RESPONSE_OPS;
41161
+ var ProtocolError3, FORBIDDEN_KEYS, BASE64_RE2, SCOPE_KEY_RE, SCOPE_KEY_GLOB_RE, HEADER_NAME_RE, HOSTNAME_RE, CAPTURE_PATH_RE, DOM_SELECTOR_RE, DOM_ATTRIBUTE_RE, GRAPHQL_OP_NAME_RE, PAIR_CODE_RE, DISPLAY_FORBIDDEN_RE, ACCOUNT_KEY_FIELDS, ACCOUNT_ATTEST_FIELDS, TYPE_ONLY_FIELDS, BRIDGE_ROLE_FIELDS, BRIDGE_ROLE_SERVING_FIELDS, KNOWN_RESPONSE_OPS;
41083
41162
  var init_validate = __esm({
41084
41163
  "node_modules/@fetchproxy/protocol/dist/validate.js"() {
41085
41164
  init_frames();
41086
41165
  init_capability_availability();
41166
+ init_binary_fetch();
41087
41167
  init_mcp_id();
41088
41168
  init_account_attest();
41089
41169
  init_json_pointer();
@@ -41096,7 +41176,7 @@ var init_validate = __esm({
41096
41176
  }
41097
41177
  };
41098
41178
  FORBIDDEN_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
41099
- BASE64_RE = /^[A-Za-z0-9+/]*={0,2}$/;
41179
+ BASE64_RE2 = /^[A-Za-z0-9+/]*={0,2}$/;
41100
41180
  SCOPE_KEY_RE = /^[A-Za-z0-9_.\-]{1,256}$/;
41101
41181
  SCOPE_KEY_GLOB_RE = /^[A-Za-z0-9_.\-]{1,256}\*?$/;
41102
41182
  HEADER_NAME_RE = /^[A-Za-z0-9_\-]{1,128}$/;
@@ -41260,6 +41340,7 @@ var init_dist3 = __esm({
41260
41340
  init_account_attest();
41261
41341
  init_awaiting_approval();
41262
41342
  init_capability_availability();
41343
+ init_binary_fetch();
41263
41344
  init_validate();
41264
41345
  init_room_frames();
41265
41346
  init_crypto();
@@ -41283,7 +41364,30 @@ function sessionInfoFromHello(hello) {
41283
41364
  known.add(c);
41284
41365
  }
41285
41366
  }
41286
- return { platform: hello.platform, unavailableCapabilities: [...known].sort() };
41367
+ const features = /* @__PURE__ */ new Set();
41368
+ const rawFeatures = hello.features;
41369
+ if (Array.isArray(rawFeatures)) {
41370
+ for (const f of rawFeatures) {
41371
+ if (typeof f === "string" && KNOWN_EXTENSION_FEATURES.has(f))
41372
+ features.add(f);
41373
+ }
41374
+ }
41375
+ return {
41376
+ platform: hello.platform,
41377
+ unavailableCapabilities: [...known].sort(),
41378
+ features: [...features].sort()
41379
+ };
41380
+ }
41381
+ function featureForRequest(inner) {
41382
+ if (inner.type !== "request" || inner.op !== "fetch")
41383
+ return null;
41384
+ return inner.init.responseType === "base64" ? EXTENSION_FEATURE_FETCH_BASE64 : null;
41385
+ }
41386
+ function missingFeatureFor(inner, info) {
41387
+ const feature = featureForRequest(inner);
41388
+ if (feature === null || info === null)
41389
+ return null;
41390
+ return info.features.includes(feature) ? null : feature;
41287
41391
  }
41288
41392
  function capabilitiesForRequest(inner) {
41289
41393
  if (inner.type !== "request")
@@ -45113,6 +45217,8 @@ async function buildServerHello(opts) {
45113
45217
  };
45114
45218
  if (opts.accepts && opts.accepts.length > 0)
45115
45219
  hello.accepts = [...opts.accepts];
45220
+ if (opts.tabDomains && opts.tabDomains.length > 0)
45221
+ hello.tabDomains = [...opts.tabDomains];
45116
45222
  if (opts.cookieKeys && opts.cookieKeys.length > 0) {
45117
45223
  hello.cookieKeys = [...opts.cookieKeys];
45118
45224
  }
@@ -45950,7 +46056,8 @@ async function startHost(opts) {
45950
46056
  sessionStoragePointers: opts.ownSessionStoragePointers,
45951
46057
  domSelectors: opts.ownDomSelectors,
45952
46058
  domListSelectors: opts.ownDomListSelectors,
45953
- graphqlOps: opts.ownGraphqlOps
46059
+ graphqlOps: opts.ownGraphqlOps,
46060
+ tabDomains: opts.ownTabDomains
45954
46061
  };
45955
46062
  const generateSessionKeypair = opts.generateSessionKeypair ?? generateX25519;
45956
46063
  let extensionWs = null;
@@ -46536,6 +46643,7 @@ async function startPeer(opts) {
46536
46643
  localStoragePointers: opts.localStoragePointers,
46537
46644
  sessionStoragePointers: opts.sessionStoragePointers,
46538
46645
  graphqlOps: opts.graphqlOps,
46646
+ tabDomains: opts.tabDomains,
46539
46647
  // 2.5.0: let a host ≥2.5.0 tell us when the extension leaves, so
46540
46648
  // `extensionConnected()` / `sessionLinked()` can go back to false.
46541
46649
  // 2.6.0: `hello-rejected` for the same reason, from the extension.
@@ -46914,6 +47022,8 @@ var init_peer = __esm({
46914
47022
  function classifyFetchError(error62, code2) {
46915
47023
  if (code2 === CAPABILITY_UNAVAILABLE_CODE)
46916
47024
  return "capability_unavailable";
47025
+ if (code2 === RESPONSE_TOO_LARGE_CODE)
47026
+ return "body_too_large";
46917
47027
  if (/^(in-page )?fetch threw:/.test(error62)) {
46918
47028
  return "tab_fetch_failed";
46919
47029
  }
@@ -47013,6 +47123,13 @@ function protocolErrorFrom(error62, op, response) {
47013
47123
  platform: unavailable?.platform ?? null
47014
47124
  });
47015
47125
  }
47126
+ const tooLarge = parseResponseTooLarge(error62);
47127
+ if (tooLarge !== null || response?.code === RESPONSE_TOO_LARGE_CODE) {
47128
+ return new FetchproxyResponseTooLargeError(error62, {
47129
+ limitBytes: tooLarge?.limitBytes ?? null,
47130
+ declaredBytes: tooLarge?.declaredBytes ?? null
47131
+ });
47132
+ }
47016
47133
  if (error62 === "timeout")
47017
47134
  return new FetchproxyWaitedError(error62, op);
47018
47135
  if (SCOPE_REJECTION.test(error62))
@@ -47047,6 +47164,13 @@ function assertSubdomainLabel(label) {
47047
47164
  throw new Error(`FetchproxyServer: subdomain must be a DNS label like "www" or "api" (or dot-separated like "auth.api"), got ${JSON.stringify(label)}`);
47048
47165
  }
47049
47166
  }
47167
+ function assertExpectedStatus(response, expected, asHttp = () => response) {
47168
+ if (expected === void 0)
47169
+ return;
47170
+ const matched = Array.isArray(expected) ? expected.includes(response.status) : response.status === expected;
47171
+ if (!matched)
47172
+ throw new FetchproxyHttpError(asHttp());
47173
+ }
47050
47174
  function assertUrlInDomains(field, url2, domains) {
47051
47175
  let parsed;
47052
47176
  try {
@@ -47091,7 +47215,7 @@ function isResendableRequest(inner) {
47091
47215
  return true;
47092
47216
  }
47093
47217
  }
47094
- var FetchproxyProtocolError, FetchproxyHttpError, FetchproxyBridgeDownError, FetchproxyHintedError, FetchproxyScopeError, FetchproxyNoTabError, FetchproxyTabOpeningError, FetchproxyWaitedError, FetchproxyCapabilityUnavailableError, SCOPE_REJECTION, NO_TAB_REJECTION, TAB_OPENING_REJECTION, VERB_DEADLINE_GRACE_MS, FetchproxyTimeoutError, TIMEOUT_NOT_RESENT_NOTE, SUBDOMAIN_LABEL_RE, DEFAULT_JSON_OK_STATUSES, TIMEOUT_RETRY_SAFE_METHODS, FetchproxyServer;
47218
+ var FetchproxyProtocolError, FetchproxyHttpError, FetchproxyBridgeDownError, FetchproxyHintedError, FetchproxyScopeError, FetchproxyNoTabError, FetchproxyTabOpeningError, FetchproxyWaitedError, FetchproxyCapabilityUnavailableError, FetchproxyExtensionUpdateRequiredError, FetchproxyResponseTooLargeError, SCOPE_REJECTION, NO_TAB_REJECTION, TAB_OPENING_REJECTION, VERB_DEADLINE_GRACE_MS, FetchproxyTimeoutError, TIMEOUT_NOT_RESENT_NOTE, SUBDOMAIN_LABEL_RE, DEFAULT_JSON_OK_STATUSES, TIMEOUT_RETRY_SAFE_METHODS, FetchproxyServer;
47095
47219
  var init_ws_server = __esm({
47096
47220
  "node_modules/@fetchproxy/server/dist/ws-server.js"() {
47097
47221
  init_dist3();
@@ -47194,6 +47318,28 @@ var init_ws_server = __esm({
47194
47318
  this.platform = info.platform;
47195
47319
  }
47196
47320
  };
47321
+ FetchproxyExtensionUpdateRequiredError = class extends FetchproxyHintedError {
47322
+ /** The feature the extension lacks (`'fetch_base64'`). */
47323
+ feature;
47324
+ constructor(originalError, info) {
47325
+ super(originalError, `this MCP needs the browser extension to support "${info.feature}" (binary downloads through the bridge), and the ContextMint Bridge build it is paired with does not. Update ContextMint Bridge (Chrome Web Store / App Store, or rebuild a sideloaded copy and reload it), then retry. Nothing is wrong with the MCP or your pairing, and re-pairing will not change it.`);
47326
+ this.name = "FetchproxyExtensionUpdateRequiredError";
47327
+ this.feature = info.feature;
47328
+ }
47329
+ };
47330
+ FetchproxyResponseTooLargeError = class extends FetchproxyHintedError {
47331
+ /** The cap the extension enforced, in bytes, when it said; else `null`. */
47332
+ limitBytes;
47333
+ /** The response's `content-length`, when the refusal came from it; else `null`. */
47334
+ declaredBytes;
47335
+ constructor(originalError, info) {
47336
+ const cap = info.limitBytes ?? MAX_BINARY_RESPONSE_BYTES;
47337
+ super(originalError, `the file is larger than the ${cap}-byte cap one bridge response can carry, so the extension refused it rather than send a frame the bridge cannot deliver. This is not a version or pairing problem: fetch a smaller rendition if the site offers one, or download it directly in the browser.`);
47338
+ this.name = "FetchproxyResponseTooLargeError";
47339
+ this.limitBytes = info.limitBytes;
47340
+ this.declaredBytes = info.declaredBytes;
47341
+ }
47342
+ };
47197
47343
  SCOPE_REJECTION = /not in declared/;
47198
47344
  NO_TAB_REJECTION = /no tab matching (?!.*(content script loaded|one is still opening))/;
47199
47345
  TAB_OPENING_REJECTION = /no tab matching .*one is still opening/;
@@ -47360,6 +47506,9 @@ var init_ws_server = __esm({
47360
47506
  throw new Error(`FetchproxyServer: opts.domains entry ${JSON.stringify(d)} is a public suffix \u2014 declare a registrable domain (e.g. "example.${d}"), not the suffix every site under it shares`);
47361
47507
  }
47362
47508
  }
47509
+ if (opts.tabDomains !== void 0) {
47510
+ assertTabDomains(opts.tabDomains, opts.domains, "FetchproxyServer: opts.tabDomains");
47511
+ }
47363
47512
  let capabilities;
47364
47513
  if (opts.capabilities === void 0) {
47365
47514
  capabilities = ["fetch"];
@@ -47430,6 +47579,7 @@ var init_ws_server = __esm({
47430
47579
  name: d.name,
47431
47580
  operationName: d.operationName
47432
47581
  })),
47582
+ ...opts.tabDomains !== void 0 ? { tabDomains: [...opts.tabDomains] } : {},
47433
47583
  // 0.8.0+: timer + lazy-revive default to ON. Every realty MCP
47434
47584
  // adapter was about to set these to the same numbers anyway; the
47435
47585
  // back-door is `0` (explicit opt-out) if a caller genuinely wants
@@ -47559,6 +47709,7 @@ var init_ws_server = __esm({
47559
47709
  ownDomSelectors: this.opts.domSelectors,
47560
47710
  ownDomListSelectors: this.opts.domListSelectors,
47561
47711
  ownGraphqlOps: this.opts.graphqlOps,
47712
+ ownTabDomains: this.opts.tabDomains,
47562
47713
  onPairCode: this.opts.onPairCode,
47563
47714
  extensionTrust: this.extensionTrust()
47564
47715
  });
@@ -47592,6 +47743,7 @@ var init_ws_server = __esm({
47592
47743
  domSelectors: this.opts.domSelectors,
47593
47744
  domListSelectors: this.opts.domListSelectors,
47594
47745
  graphqlOps: this.opts.graphqlOps,
47746
+ tabDomains: this.opts.tabDomains,
47595
47747
  extensionTrust: this.extensionTrust(),
47596
47748
  requireExtensionIdentity: this.opts.requireExtensionIdentity
47597
47749
  });
@@ -47876,6 +48028,15 @@ var init_ws_server = __esm({
47876
48028
  cause: err
47877
48029
  };
47878
48030
  }
48031
+ if (err instanceof FetchproxyExtensionUpdateRequiredError) {
48032
+ return {
48033
+ ok: false,
48034
+ error: err.originalError,
48035
+ kind: "extension_update_required",
48036
+ retryAttempted: false,
48037
+ cause: err
48038
+ };
48039
+ }
47879
48040
  return {
47880
48041
  ok: false,
47881
48042
  error: err instanceof Error ? err.message : String(err),
@@ -48022,7 +48183,10 @@ var init_ws_server = __esm({
48022
48183
  if (result.kind === "capability_unavailable") {
48023
48184
  return protocolErrorFrom(result.error, void 0, { code: CAPABILITY_UNAVAILABLE_CODE, op });
48024
48185
  }
48025
- return protocolErrorFrom(result.error);
48186
+ if (result.kind === "extension_update_required" && result.cause instanceof FetchproxyExtensionUpdateRequiredError) {
48187
+ return result.cause;
48188
+ }
48189
+ return protocolErrorFrom(result.error, void 0, result.code !== void 0 ? { code: result.code, op } : void 0);
48026
48190
  }
48027
48191
  /**
48028
48192
  * Convenience wrapper around `fetch()`. Builds the URL from a path
@@ -48042,6 +48206,88 @@ var init_ws_server = __esm({
48042
48206
  * the declared domains exactly.
48043
48207
  */
48044
48208
  async request(method, path2, opts = {}) {
48209
+ const init = this.resolveRequestInit("request", method, path2, opts);
48210
+ const result = await this.fetch(init, opts.retryOnTimeout !== void 0 ? { retryOnTimeout: opts.retryOnTimeout } : {});
48211
+ if (!result.ok)
48212
+ this.throwFetchFailure(result, init.url, method, opts.retryOnTimeout);
48213
+ const response = {
48214
+ status: result.status,
48215
+ body: result.body,
48216
+ url: result.url
48217
+ };
48218
+ assertExpectedStatus(response, opts.expectStatus);
48219
+ return response;
48220
+ }
48221
+ /**
48222
+ * #460: like `request()`, but the response body comes back as BYTES,
48223
+ * intact — a PDF, an office file, an image — together with the relayed
48224
+ * `content-type` / `content-length` / `content-disposition` headers and the
48225
+ * final URL after redirects.
48226
+ *
48227
+ * The extension runs it from its background worker rather than a tab, so a
48228
+ * file host that sends no CORS headers is still readable, and it follows
48229
+ * redirects. Both the request URL and the FINAL URL must be inside the
48230
+ * declared `domains` — the extension enforces that, and so does this method
48231
+ * on the answer. To read a signed file host, declare it.
48232
+ *
48233
+ * Bodies are capped at `MAX_BINARY_RESPONSE_BYTES` (16 MiB): over it the
48234
+ * extension refuses with `FetchproxyResponseTooLargeError`. An extension
48235
+ * that predates binary fetch is refused with
48236
+ * `FetchproxyExtensionUpdateRequiredError` — locally when its hello says so,
48237
+ * and on the answer when it replied in text anyway.
48238
+ */
48239
+ async requestBytes(method, path2, opts = {}) {
48240
+ const init = {
48241
+ ...this.resolveRequestInit("requestBytes", method, path2, opts),
48242
+ responseType: "base64"
48243
+ };
48244
+ const result = await this.fetch(init, opts.retryOnTimeout !== void 0 ? { retryOnTimeout: opts.retryOnTimeout } : {});
48245
+ if (!result.ok)
48246
+ this.throwFetchFailure(result, init.url, method, opts.retryOnTimeout);
48247
+ if (result.bodyEncoding !== "base64") {
48248
+ throw new FetchproxyExtensionUpdateRequiredError("the paired ContextMint Bridge extension answered a binary request as text", { feature: EXTENSION_FEATURE_FETCH_BASE64 });
48249
+ }
48250
+ try {
48251
+ assertUrlInDomains("requestBytes final url", result.url, this.opts.domains);
48252
+ } catch (err) {
48253
+ throw new FetchproxyProtocolError(err.message);
48254
+ }
48255
+ if (!isBase64Body(result.body)) {
48256
+ throw new FetchproxyProtocolError("requestBytes: the extension sent a body that is not base64");
48257
+ }
48258
+ const decoded = Buffer.from(result.body, "base64");
48259
+ const response = {
48260
+ status: result.status,
48261
+ url: result.url,
48262
+ headers: { ...result.headers ?? {} },
48263
+ bytes: new Uint8Array(decoded.buffer, decoded.byteOffset, decoded.byteLength)
48264
+ };
48265
+ assertExpectedStatus(response, opts.expectStatus, () => ({
48266
+ status: response.status,
48267
+ url: response.url,
48268
+ // FetchproxyHttpError carries an HttpResponse; a binary body has no
48269
+ // honest text form, so the bytes stay on the BinaryResponse.
48270
+ body: ""
48271
+ }));
48272
+ return response;
48273
+ }
48274
+ /** #460: `requestBytes('GET', path, opts)`. */
48275
+ getBytes(path2, opts = {}) {
48276
+ return this.requestBytes("GET", path2, opts);
48277
+ }
48278
+ /**
48279
+ * Resolve `path` + options into the wire `FetchInit` — URL building, the
48280
+ * declared-domain guard, the relay tab. Shared by `request()` and
48281
+ * `requestBytes()` so the two can never disagree about which URLs are
48282
+ * reachable.
48283
+ *
48284
+ * Path resolution:
48285
+ * - Absolute URL (`https://...`) → used as-is (still guarded against
48286
+ * leaving the declared domain set).
48287
+ * - Relative path → joined with `https://${subdomain}.${baseDomain}`
48288
+ * (or `https://${baseDomain}` if no subdomain is given).
48289
+ */
48290
+ resolveRequestInit(caller, method, path2, opts) {
48045
48291
  if (opts.subdomain !== void 0)
48046
48292
  assertSubdomainLabel(opts.subdomain);
48047
48293
  const baseDomain = this.resolveBaseDomain(opts.domain);
@@ -48051,24 +48297,24 @@ var init_ws_server = __esm({
48051
48297
  try {
48052
48298
  host = new URL(path2).host;
48053
48299
  } catch {
48054
- throw new Error(`FetchproxyServer.request: absolute path is not a valid URL: ${JSON.stringify(path2)}`);
48300
+ throw new Error(`FetchproxyServer.${caller}: absolute path is not a valid URL: ${JSON.stringify(path2)}`);
48055
48301
  }
48056
48302
  } else {
48057
48303
  host = opts.subdomain ? `${opts.subdomain}.${baseDomain}` : baseDomain;
48058
48304
  }
48059
48305
  const url2 = isAbsolute6 ? path2 : `https://${host}${path2}`;
48060
- assertUrlInDomains("request url", url2, this.opts.domains);
48306
+ assertUrlInDomains(`${caller} url`, url2, this.opts.domains);
48061
48307
  let tabUrl = `https://${host}/`;
48062
48308
  if (opts.viaTab !== void 0) {
48063
48309
  try {
48064
48310
  new URL(opts.viaTab);
48065
48311
  } catch {
48066
- throw new Error(`FetchproxyServer.request: viaTab is not a valid URL: ${JSON.stringify(opts.viaTab)}`);
48312
+ throw new Error(`FetchproxyServer.${caller}: viaTab is not a valid URL: ${JSON.stringify(opts.viaTab)}`);
48067
48313
  }
48068
48314
  assertUrlInDomains("viaTab", opts.viaTab, this.opts.domains);
48069
48315
  tabUrl = opts.viaTab;
48070
48316
  }
48071
- const init = {
48317
+ return {
48072
48318
  url: url2,
48073
48319
  method,
48074
48320
  tabUrl,
@@ -48083,29 +48329,16 @@ var init_ws_server = __esm({
48083
48329
  // every version before this option existed.
48084
48330
  ...opts.credentials === "omit" ? { credentials: "omit" } : {}
48085
48331
  };
48086
- const result = await this.fetch(init, opts.retryOnTimeout !== void 0 ? { retryOnTimeout: opts.retryOnTimeout } : {});
48087
- if (!result.ok) {
48088
- if (result.kind === "session_not_ready" && result.cause instanceof Error) {
48089
- throw result.cause;
48090
- }
48091
- if (result.kind === "capability_unavailable" && result.cause instanceof FetchproxyCapabilityUnavailableError) {
48092
- throw result.cause;
48093
- }
48094
- throw this._typedErrorFor(result, init.url, "fetch", result.retryAttempted ?? false, isRetrySafeOnTimeout(method, opts.retryOnTimeout));
48332
+ }
48333
+ /** Turn a failed `fetch()` envelope into the throw `request()` has always made. */
48334
+ throwFetchFailure(result, url2, method, retryOnTimeout) {
48335
+ if (result.kind === "session_not_ready" && result.cause instanceof Error) {
48336
+ throw result.cause;
48095
48337
  }
48096
- const response = {
48097
- status: result.status,
48098
- body: result.body,
48099
- url: result.url
48100
- };
48101
- if (opts.expectStatus !== void 0) {
48102
- const expected = opts.expectStatus;
48103
- const matched = Array.isArray(expected) ? expected.includes(response.status) : response.status === expected;
48104
- if (!matched) {
48105
- throw new FetchproxyHttpError(response);
48106
- }
48338
+ if (result.kind === "capability_unavailable" && result.cause instanceof FetchproxyCapabilityUnavailableError) {
48339
+ throw result.cause;
48107
48340
  }
48108
- return response;
48341
+ throw this._typedErrorFor(result, url2, "fetch", result.retryAttempted ?? false, isRetrySafeOnTimeout(method, retryOnTimeout));
48109
48342
  }
48110
48343
  /** Issue a GET against `path` (resolved via `request()` rules). */
48111
48344
  get(path2, opts = {}) {
@@ -48906,6 +49139,10 @@ var init_ws_server = __esm({
48906
49139
  status: inner.status,
48907
49140
  url: inner.url,
48908
49141
  body: inner.body,
49142
+ // #460: carried only when present, so a text answer's envelope
49143
+ // has exactly the keys it always had.
49144
+ ...inner.bodyEncoding !== void 0 ? { bodyEncoding: inner.bodyEncoding } : {},
49145
+ ...inner.headers !== void 0 ? { headers: { ...inner.headers } } : {},
48909
49146
  retryAttempted: false
48910
49147
  });
48911
49148
  } else {
@@ -48922,7 +49159,8 @@ var init_ws_server = __esm({
48922
49159
  ok: false,
48923
49160
  error: inner.error,
48924
49161
  kind: classifyFetchError(inner.error, inner.code),
48925
- retryAttempted: false
49162
+ retryAttempted: false,
49163
+ ...inner.code !== void 0 ? { code: inner.code } : {}
48926
49164
  });
48927
49165
  }
48928
49166
  return;
@@ -49226,6 +49464,7 @@ var init_ws_server = __esm({
49226
49464
  pairCode,
49227
49465
  extensionConnected: false,
49228
49466
  unavailableCapabilities: [],
49467
+ features: [],
49229
49468
  platform: null
49230
49469
  };
49231
49470
  }
@@ -49237,6 +49476,7 @@ var init_ws_server = __esm({
49237
49476
  pairCode,
49238
49477
  extensionConnected,
49239
49478
  unavailableCapabilities: info ? [...info.unavailableCapabilities] : [],
49479
+ features: info ? [...info.features] : [],
49240
49480
  platform: info?.platform ?? null
49241
49481
  };
49242
49482
  }
@@ -49250,9 +49490,13 @@ var init_ws_server = __esm({
49250
49490
  */
49251
49491
  refuseIfUnavailable(inner, info) {
49252
49492
  const cap = unavailableCapabilityFor(inner, info);
49253
- if (cap === null || info === null)
49254
- return;
49255
- throw new FetchproxyCapabilityUnavailableError(capabilityUnavailableMessage(cap, info.platform), { capability: cap, platform: info.platform });
49493
+ if (cap !== null && info !== null) {
49494
+ throw new FetchproxyCapabilityUnavailableError(capabilityUnavailableMessage(cap, info.platform), { capability: cap, platform: info.platform });
49495
+ }
49496
+ const feature = missingFeatureFor(inner, info);
49497
+ if (feature !== null) {
49498
+ throw new FetchproxyExtensionUpdateRequiredError(`the paired ContextMint Bridge extension does not support "${feature}"`, { feature });
49499
+ }
49256
49500
  }
49257
49501
  /**
49258
49502
  * 0.5.2+: throw `FetchproxyProtocolError` with the actionable pair-code
@@ -49353,6 +49597,7 @@ var init_dist4 = __esm({
49353
49597
  init_deadline();
49354
49598
  init_parse_html();
49355
49599
  init_batch();
49600
+ init_dist3();
49356
49601
  init_extension_trust();
49357
49602
  init_extension_pins();
49358
49603
  init_identity();
@@ -68470,13 +68715,17 @@ function compactEvent(e) {
68470
68715
  ShowAs: e.ShowAs
68471
68716
  });
68472
68717
  }
68718
+ function eventAttendees(e) {
68719
+ return e.Attendees?.map((a) => pruned({ Who: addr(a), Response: a.Status?.Response }));
68720
+ }
68721
+ function detailedEvent(e) {
68722
+ return pruned({ ...compactEvent(e), Attendees: eventAttendees(e) });
68723
+ }
68473
68724
  function fullEvent(e) {
68474
68725
  return pruned({
68475
68726
  ...compactEvent(e),
68476
68727
  SeriesMasterId: e.SeriesMasterId,
68477
- Attendees: e.Attendees?.map(
68478
- (a) => pruned({ Who: addr(a), Response: a.Status?.Response })
68479
- ),
68728
+ Attendees: eventAttendees(e),
68480
68729
  // The Teams link lives under OnlineMeeting; OnlineMeetingUrl is empty on
68481
68730
  // every live Teams meeting checked.
68482
68731
  JoinUrl: e.OnlineMeeting?.JoinUrl || e.OnlineMeetingUrl || void 0,
@@ -69199,7 +69448,7 @@ function registerCalendarTools(server, client2) {
69199
69448
  server.registerTool(
69200
69449
  "outlook_get_event",
69201
69450
  {
69202
- description: `Get one calendar event in full, including attendees and their responses. Attendees appear in view:"full", every one of them \u2014 there is no 20-address cap as on a message's To/Cc; view:"raw" returns Outlook's untouched record, uncapped, Attendees included. ` + UNTRUSTED_DESCRIPTION_SUFFIX,
69451
+ description: 'Get one calendar event with its attendees and their responses. Attendees come back in EVERY view, including the default compact one \u2014 all of them, uncapped (no 20-address cap as on a message\'s To/Cc). The event\'s time is labelled with its zone in the `TimeZone` field (the mailbox zone unless you pass `timeZone`); the same instant read in another zone shows a different clock time, so read `TimeZone`, not just the clock. view:"full" adds the Teams join link, body preview and web link; view:"raw" returns Outlook\'s untouched record. ' + UNTRUSTED_DESCRIPTION_SUFFIX,
69203
69452
  annotations: { readOnlyHint: true, openWorldHint: true },
69204
69453
  _meta: workMeta("lookup"),
69205
69454
  inputSchema: external_exports.object({
@@ -69216,7 +69465,7 @@ function registerCalendarTools(server, client2) {
69216
69465
  );
69217
69466
  const v = resolveView(view, VIEWS2);
69218
69467
  if (v === "raw") return mailboxUntrusted(data);
69219
- return mailboxUntrusted(v === "compact" ? compactEvent(data) : fullEvent(data));
69468
+ return mailboxUntrusted(v === "full" ? fullEvent(data) : detailedEvent(data));
69220
69469
  }
69221
69470
  );
69222
69471
  server.registerTool(
@@ -71631,7 +71880,7 @@ function decodeJwt(token) {
71631
71880
  return { oid: claims.oid, tid: claims.tid };
71632
71881
  }
71633
71882
  function cleanAnswer(text) {
71634
- return text.replace(/[\u{E000}-\u{F8FF}\u{F0000}-\u{FFFFD}\u{100000}-\u{10FFFD}]/gu, "").replace(/cite(?:turn\d+[a-z]+\d+)+/gi, "").replace(/【[^】]{1,60}】/g, "").replace(/[ \t]+\n/g, "\n").replace(/[ \t]{2,}/g, " ").trim();
71883
+ return text.replace(/[\u{E000}-\u{F8FF}\u{F0000}-\u{FFFFD}\u{100000}-\u{10FFFD}]/gu, "").replace(/cite(?:turn\d+[a-z]+\d+)+/gi, "").replace(/【[^】]{1,60}】/g, "").replace(/[ \t]+\n/g, "\n").replace(/(\S)[ \t]{2,}/g, "$1 ").trim();
71635
71884
  }
71636
71885
  var DisengagedError = class extends Error {
71637
71886
  disengaged = true;
@@ -71898,6 +72147,12 @@ function abortError() {
71898
72147
  }
71899
72148
 
71900
72149
  // src/tools/copilot.ts
72150
+ async function timeContext(outlook) {
72151
+ const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
72152
+ const zone = await mailboxTimeZone(outlook);
72153
+ return zone ? `(Context: today is ${today}. The user's mailbox time zone is ${zone}, and mailbox times are stored in UTC. When you state any date or time, convert it to ${zone} and label the zone.)` : `(Context: today is ${today}. Mailbox times are stored in UTC \u2014 when you state a time, label its time zone.)`;
72154
+ }
72155
+ var VERBATIM_DIRECTIVE = "(Reproduce any content I ask you to quote or return IN FULL and verbatim \u2014 do not summarise, paraphrase, shorten, or omit any part of it, even if it is long.)";
71901
72156
  var TURN_TIMEOUT_MS = 12e4;
71902
72157
  function turnSignal(hostSignal) {
71903
72158
  const controller = new AbortController();
@@ -71916,23 +72171,27 @@ function turnSignal(hostSignal) {
71916
72171
  }
71917
72172
  };
71918
72173
  }
71919
- function registerCopilotTools(server, copilot2) {
72174
+ function registerCopilotTools(server, copilot2, outlook) {
71920
72175
  server.registerTool(
71921
72176
  "outlook_ask_copilot",
71922
72177
  {
71923
- description: 'Ask Microsoft 365 Copilot a question answered from the signed-in mailbox (Copilot searches your own mail, calendar and files server-side and writes a reply). Use it for synthesis across many messages \u2014 "what needs my attention first", "summarise the thread with X", "did anyone ask me for anything this week". Follow-up calls continue the same conversation; pass newConversation:true to start fresh. The prompt is sent to Microsoft\'s Copilot service on your tenant; put your own question in it, not text copied from an email. Each call uses one of a per-conversation quota, reported back. First use needs a one-time sign-in: run `opencode-m365 login` (from the opencode-m365-copilot package) so an MSAL cache exists.' + UNTRUSTED_DESCRIPTION_SUFFIX,
72178
+ description: 'Ask Microsoft 365 Copilot a question answered from the signed-in mailbox (Copilot searches your own mail, calendar and files server-side and writes a reply). Use it for synthesis across many messages \u2014 "what needs my attention first", "summarise the thread with X", "did anyone ask me for anything this week". Follow-up calls continue the same conversation; pass newConversation:true to start fresh. Pass verbatim:true when you need content quoted back in full (Copilot otherwise answers concisely and summarises long material; for a single item a focused "quote X in full" also works). Times are converted to the mailbox time zone and labelled. The prompt is sent to Microsoft\'s Copilot service on your tenant; put your own question in it, not text copied from an email. Each call uses one of a per-conversation quota, reported back. First use needs a one-time sign-in: run `opencode-m365 login` (from the opencode-m365-copilot package) so an MSAL cache exists.' + UNTRUSTED_DESCRIPTION_SUFFIX,
71924
72179
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
71925
72180
  _meta: workMeta("lookup", { aliases: ["copilot", "ask copilot", "m365 copilot"] }),
71926
72181
  inputSchema: external_exports.object({
71927
72182
  prompt: external_exports.string().min(1).describe("The question for Copilot, in your own words"),
71928
- newConversation: external_exports.boolean().optional().describe("Start a fresh conversation with no prior context (default false: continue the current one)")
72183
+ newConversation: external_exports.boolean().optional().describe("Start a fresh conversation with no prior context (default false: continue the current one)"),
72184
+ verbatim: external_exports.boolean().optional().describe("Ask Copilot to quote requested content in full rather than summarising (default false). Use for retrieving a saved note, a full message, etc.")
71929
72185
  })
71930
72186
  },
71931
- async ({ prompt, newConversation }, ctx) => {
72187
+ async ({ prompt, newConversation, verbatim }, ctx) => {
71932
72188
  if (newConversation) copilot2.reset();
71933
72189
  const { signal, dispose } = turnSignal(ctx?.signal);
71934
72190
  try {
71935
- const result = await copilot2.ask(prompt, { signal });
72191
+ const preamble = [await timeContext(outlook), verbatim ? VERBATIM_DIRECTIVE : ""].filter(Boolean).join(" ");
72192
+ const result = await copilot2.ask(`${preamble}
72193
+
72194
+ ${prompt}`, { signal });
71936
72195
  return mailboxUntrusted({
71937
72196
  answer: result.text,
71938
72197
  conversationId: copilot2.conversationId,
@@ -72090,7 +72349,7 @@ await runMcp({
72090
72349
  registerWriteTools,
72091
72350
  registerExportTools,
72092
72351
  registerRuleTools,
72093
- (server) => registerCopilotTools(server, copilot),
72352
+ (server) => registerCopilotTools(server, copilot, client),
72094
72353
  registerHealthcheckTool
72095
72354
  ]
72096
72355
  });
@@ -98,7 +98,10 @@ export function cleanAnswer(text) {
98
98
  .replace(/cite(?:turn\d+[a-z]+\d+)+/gi, '')
99
99
  .replace(/【[^】]{1,60}】/g, '')
100
100
  .replace(/[ \t]+\n/g, '\n')
101
- .replace(/[ \t]{2,}/g, ' ')
101
+ // Collapse only runs that FOLLOW a non-space character (the double spaces a
102
+ // removed token leaves mid-line), never leading indentation — a Copilot
103
+ // answer can quote an indented code block from a mail or note.
104
+ .replace(/(\S)[ \t]{2,}/g, '$1 ')
102
105
  .trim();
103
106
  }
104
107
  /** M365 refused to engage with the prompt. Not a rate limit — do not retry into it. */
package/dist/index.js CHANGED
@@ -45,7 +45,7 @@ await runMcp({
45
45
  registerWriteTools,
46
46
  registerExportTools,
47
47
  registerRuleTools,
48
- (server) => registerCopilotTools(server, copilot),
48
+ (server) => registerCopilotTools(server, copilot, client),
49
49
  registerHealthcheckTool,
50
50
  ],
51
51
  });
@@ -2,7 +2,7 @@ import { z } from 'zod';
2
2
  import { McpToolError, minifiedResult, resolveView, viewParam } from '@chrischall/mcp-utils';
3
3
  import { mailboxUntrusted, UNTRUSTED_DESCRIPTION_SUFFIX } from './_untrusted.js';
4
4
  import { briefDate, briefLine, briefResult, briefViewParam, spaced, workMeta } from './_hints.js';
5
- import { compactEvent, fullEvent, projectCollection, VIEWS, } from '../view.js';
5
+ import { compactEvent, detailedEvent, fullEvent, projectCollection, VIEWS, } from '../view.js';
6
6
  import { fetchPage, nextLinkParam, plainCollection } from './_paging.js';
7
7
  import { mailboxTimeZone } from '../timezone.js';
8
8
  const EVENT_SELECT = 'Id,Subject,Start,End,Location,Organizer,IsAllDay,IsCancelled,ShowAs,OnlineMeetingUrl,OnlineMeeting,BodyPreview,' +
@@ -89,7 +89,7 @@ export function registerCalendarTools(server, client) {
89
89
  return mailboxUntrusted(projectCollection(data, v === 'full' ? fullEvent : compactEvent, 'event'));
90
90
  });
91
91
  server.registerTool('outlook_get_event', {
92
- description: 'Get one calendar event in full, including attendees and their responses. Attendees appear in view:"full", every one of them — there is no 20-address cap as on a message\'s To/Cc; view:"raw" returns Outlook\'s untouched record, uncapped, Attendees included.' +
92
+ description: 'Get one calendar event with its attendees and their responses. Attendees come back in EVERY view, including the default compact one — all of them, uncapped (no 20-address cap as on a message\'s To/Cc). The event\'s time is labelled with its zone in the `TimeZone` field (the mailbox zone unless you pass `timeZone`); the same instant read in another zone shows a different clock time, so read `TimeZone`, not just the clock. view:"full" adds the Teams join link, body preview and web link; view:"raw" returns Outlook\'s untouched record.' +
93
93
  ' ' + UNTRUSTED_DESCRIPTION_SUFFIX,
94
94
  annotations: { readOnlyHint: true, openWorldHint: true },
95
95
  _meta: workMeta('lookup'),
@@ -104,7 +104,7 @@ export function registerCalendarTools(server, client) {
104
104
  const v = resolveView(view, VIEWS);
105
105
  if (v === 'raw')
106
106
  return mailboxUntrusted(data);
107
- return mailboxUntrusted(v === 'compact' ? compactEvent(data) : fullEvent(data));
107
+ return mailboxUntrusted(v === 'full' ? fullEvent(data) : detailedEvent(data));
108
108
  });
109
109
  server.registerTool('outlook_list_calendars', {
110
110
  description: 'List the calendars in the mailbox, for picking one to read or write against.',
@@ -14,8 +14,28 @@
14
14
  import { z } from 'zod';
15
15
  import { minifiedResult } from '@chrischall/mcp-utils';
16
16
  import { DisengagedError } from '../copilot/session.js';
17
+ import { mailboxTimeZone } from '../timezone.js';
17
18
  import { workMeta } from './_hints.js';
18
19
  import { mailboxUntrusted, UNTRUSTED_DESCRIPTION_SUFFIX } from './_untrusted.js';
20
+ /**
21
+ * A short context preamble prepended to every Copilot prompt.
22
+ *
23
+ * Copilot renders a stored UTC time as a bare clock with no zone — a 2026-11-09
24
+ * UTC time came back as "11:41 AM", which reads as local but is not. Telling it
25
+ * today's date and the mailbox zone, and to convert and label, fixes the
26
+ * labelling at the source. The mailbox zone is read once (cached per client); if
27
+ * it cannot be read — a server set up for Copilot but not the Outlook mail
28
+ * token — the instruction degrades to "label the zone" with no date-math hint.
29
+ */
30
+ async function timeContext(outlook) {
31
+ const today = new Date().toISOString().slice(0, 10);
32
+ const zone = await mailboxTimeZone(outlook);
33
+ return zone
34
+ ? `(Context: today is ${today}. The user's mailbox time zone is ${zone}, and mailbox times are stored in UTC. When you state any date or time, convert it to ${zone} and label the zone.)`
35
+ : `(Context: today is ${today}. Mailbox times are stored in UTC — when you state a time, label its time zone.)`;
36
+ }
37
+ /** The extra instruction a verbatim request prepends. */
38
+ const VERBATIM_DIRECTIVE = "(Reproduce any content I ask you to quote or return IN FULL and verbatim — do not summarise, paraphrase, shorten, or omit any part of it, even if it is long.)";
19
39
  /** Stop a turn that outlives this, so a wedged connection cannot hang the call. */
20
40
  const TURN_TIMEOUT_MS = 120_000;
21
41
  /**
@@ -41,9 +61,9 @@ function turnSignal(hostSignal) {
41
61
  },
42
62
  };
43
63
  }
44
- export function registerCopilotTools(server, copilot) {
64
+ export function registerCopilotTools(server, copilot, outlook) {
45
65
  server.registerTool('outlook_ask_copilot', {
46
- description: 'Ask Microsoft 365 Copilot a question answered from the signed-in mailbox (Copilot searches your own mail, calendar and files server-side and writes a reply). Use it for synthesis across many messages — "what needs my attention first", "summarise the thread with X", "did anyone ask me for anything this week". Follow-up calls continue the same conversation; pass newConversation:true to start fresh. The prompt is sent to Microsoft\'s Copilot service on your tenant; put your own question in it, not text copied from an email. Each call uses one of a per-conversation quota, reported back. First use needs a one-time sign-in: run `opencode-m365 login` (from the opencode-m365-copilot package) so an MSAL cache exists.' +
66
+ description: 'Ask Microsoft 365 Copilot a question answered from the signed-in mailbox (Copilot searches your own mail, calendar and files server-side and writes a reply). Use it for synthesis across many messages — "what needs my attention first", "summarise the thread with X", "did anyone ask me for anything this week". Follow-up calls continue the same conversation; pass newConversation:true to start fresh. Pass verbatim:true when you need content quoted back in full (Copilot otherwise answers concisely and summarises long material; for a single item a focused "quote X in full" also works). Times are converted to the mailbox time zone and labelled. The prompt is sent to Microsoft\'s Copilot service on your tenant; put your own question in it, not text copied from an email. Each call uses one of a per-conversation quota, reported back. First use needs a one-time sign-in: run `opencode-m365 login` (from the opencode-m365-copilot package) so an MSAL cache exists.' +
47
67
  UNTRUSTED_DESCRIPTION_SUFFIX,
48
68
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
49
69
  _meta: workMeta('lookup', { aliases: ['copilot', 'ask copilot', 'm365 copilot'] }),
@@ -53,13 +73,20 @@ export function registerCopilotTools(server, copilot) {
53
73
  .boolean()
54
74
  .optional()
55
75
  .describe('Start a fresh conversation with no prior context (default false: continue the current one)'),
76
+ verbatim: z
77
+ .boolean()
78
+ .optional()
79
+ .describe('Ask Copilot to quote requested content in full rather than summarising (default false). Use for retrieving a saved note, a full message, etc.'),
56
80
  }),
57
- }, async ({ prompt, newConversation }, ctx) => {
81
+ }, async ({ prompt, newConversation, verbatim }, ctx) => {
58
82
  if (newConversation)
59
83
  copilot.reset();
60
84
  const { signal, dispose } = turnSignal(ctx?.signal);
61
85
  try {
62
- const result = await copilot.ask(prompt, { signal });
86
+ const preamble = [await timeContext(outlook), verbatim ? VERBATIM_DIRECTIVE : '']
87
+ .filter(Boolean)
88
+ .join(' ');
89
+ const result = await copilot.ask(`${preamble}\n\n${prompt}`, { signal });
63
90
  return mailboxUntrusted({
64
91
  answer: result.text,
65
92
  conversationId: copilot.conversationId,
package/dist/version.js CHANGED
@@ -10,4 +10,4 @@
10
10
  * line below and nowhere else — not even in prose.
11
11
  */
12
12
  export const PACKAGE_NAME = 'office-outlook-mcp';
13
- export const VERSION = '0.9.0'; // x-release-please-version
13
+ export const VERSION = '0.10.1'; // x-release-please-version
package/dist/view.js CHANGED
@@ -100,11 +100,28 @@ export function compactEvent(e) {
100
100
  ShowAs: e.ShowAs,
101
101
  });
102
102
  }
103
+ /** Attendees projected as `{ Who, Response }`, or undefined when there are none. */
104
+ export function eventAttendees(e) {
105
+ return e.Attendees?.map((a) => pruned({ Who: addr(a), Response: a.Status?.Response }));
106
+ }
107
+ /**
108
+ * A single event's compact view WITH its attendees — what outlook_get_event
109
+ * returns by default.
110
+ *
111
+ * A single event is not a list, so the per-item cap that keeps attendees off
112
+ * list_events (one `calendarview` page can hold dozens of events) does not
113
+ * apply, and the attendees are the point of looking one event up. list_events
114
+ * still uses the attendee-free {@link compactEvent}; only get_event carries them
115
+ * in its compact view, as its description promises.
116
+ */
117
+ export function detailedEvent(e) {
118
+ return pruned({ ...compactEvent(e), Attendees: eventAttendees(e) });
119
+ }
103
120
  export function fullEvent(e) {
104
121
  return pruned({
105
122
  ...compactEvent(e),
106
123
  SeriesMasterId: e.SeriesMasterId,
107
- Attendees: e.Attendees?.map((a) => pruned({ Who: addr(a), Response: a.Status?.Response })),
124
+ Attendees: eventAttendees(e),
108
125
  // The Teams link lives under OnlineMeeting; OnlineMeetingUrl is empty on
109
126
  // every live Teams meeting checked.
110
127
  JoinUrl: e.OnlineMeeting?.JoinUrl || e.OnlineMeetingUrl || undefined,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chrischall/office-outlook-mcp",
3
- "version": "0.9.0",
3
+ "version": "0.10.1",
4
4
  "mcpName": "io.github.chrischall/office-outlook-mcp",
5
5
  "description": "Outlook / Microsoft 365 MCP server for Claude — developed and maintained by AI (Claude Code)",
6
6
  "author": "Claude Code (AI) <https://www.anthropic.com/claude>",
@@ -47,20 +47,20 @@
47
47
  "dependencies": {
48
48
  "@azure/msal-node": "^7.2.1",
49
49
  "@chrischall/mcp-utils": "^3.7.2",
50
- "@fetchproxy/server": "^3.6.2",
51
- "@modelcontextprotocol/server": "^2.3.0",
52
- "dotenv": "^18.0.1",
50
+ "@fetchproxy/server": "^3.8.0",
51
+ "@modelcontextprotocol/server": "^2.4.0",
52
+ "dotenv": "^18.0.10",
53
53
  "ws": "^8.22.2",
54
- "zod": "^4.6.5"
54
+ "zod": "^4.6.7"
55
55
  },
56
56
  "devDependencies": {
57
- "@modelcontextprotocol/client": "^2.3.0",
58
- "@types/node": "^26.0.0",
57
+ "@modelcontextprotocol/client": "^2.4.0",
58
+ "@types/node": "^26.6.6",
59
59
  "@types/ws": "^8.18.2",
60
- "@vitest/coverage-v8": "^5.0.0",
61
- "esbuild": "^0.28.1",
60
+ "@vitest/coverage-v8": "^5.1.0",
61
+ "esbuild": "^0.28.2",
62
62
  "typescript": "^7.0.2",
63
- "vitest": "^5.0.0"
63
+ "vitest": "^5.1.0"
64
64
  },
65
65
  "allowScripts": {
66
66
  "esbuild": false,
package/server.json CHANGED
@@ -6,12 +6,12 @@
6
6
  "url": "https://github.com/chrischall/office-outlook-mcp",
7
7
  "source": "github"
8
8
  },
9
- "version": "0.9.0",
9
+ "version": "0.10.1",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "@chrischall/office-outlook-mcp",
14
- "version": "0.9.0",
14
+ "version": "0.10.1",
15
15
  "transport": {
16
16
  "type": "stdio"
17
17
  },