@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.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/dist/bundle.js +308 -49
- package/dist/copilot/session.js +4 -1
- package/dist/index.js +1 -1
- package/dist/tools/calendar.js +3 -3
- package/dist/tools/copilot.js +31 -4
- package/dist/version.js +1 -1
- package/dist/view.js +18 -1
- package/package.json +10 -10
- package/server.json +2 -2
|
@@ -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.
|
|
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.
|
|
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.
|
|
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.
|
|
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 (!
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
48087
|
-
|
|
48088
|
-
|
|
48089
|
-
|
|
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
|
-
|
|
48097
|
-
|
|
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
|
-
|
|
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
|
|
49254
|
-
|
|
49255
|
-
|
|
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
|
|
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:
|
|
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 === "
|
|
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
|
|
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
|
});
|
package/dist/copilot/session.js
CHANGED
|
@@ -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
|
-
|
|
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
package/dist/tools/calendar.js
CHANGED
|
@@ -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
|
|
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 === '
|
|
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.',
|
package/dist/tools/copilot.js
CHANGED
|
@@ -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
|
|
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
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
|
|
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.
|
|
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.
|
|
51
|
-
"@modelcontextprotocol/server": "^2.
|
|
52
|
-
"dotenv": "^18.0.
|
|
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.
|
|
54
|
+
"zod": "^4.6.7"
|
|
55
55
|
},
|
|
56
56
|
"devDependencies": {
|
|
57
|
-
"@modelcontextprotocol/client": "^2.
|
|
58
|
-
"@types/node": "^26.
|
|
57
|
+
"@modelcontextprotocol/client": "^2.4.0",
|
|
58
|
+
"@types/node": "^26.6.6",
|
|
59
59
|
"@types/ws": "^8.18.2",
|
|
60
|
-
"@vitest/coverage-v8": "^5.
|
|
61
|
-
"esbuild": "^0.28.
|
|
60
|
+
"@vitest/coverage-v8": "^5.1.0",
|
|
61
|
+
"esbuild": "^0.28.2",
|
|
62
62
|
"typescript": "^7.0.2",
|
|
63
|
-
"vitest": "^5.
|
|
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
|
+
"version": "0.10.1",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "@chrischall/office-outlook-mcp",
|
|
14
|
-
"version": "0.
|
|
14
|
+
"version": "0.10.1",
|
|
15
15
|
"transport": {
|
|
16
16
|
"type": "stdio"
|
|
17
17
|
},
|