@chrischall/office-outlook-mcp 0.6.1 → 0.6.2
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/auth-fetchproxy.js +26 -122
- package/dist/bundle.js +410 -209
- package/dist/client.js +45 -84
- package/dist/token-cache.js +2 -11
- package/dist/tools/writes.js +6 -16
- package/dist/version.js +1 -1
- package/package.json +2 -2
- 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.6.
|
|
10
|
+
"version": "0.6.2"
|
|
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.6.
|
|
18
|
+
"version": "0.6.2",
|
|
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.6.
|
|
4
|
+
"version": "0.6.2",
|
|
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/auth-fetchproxy.js
CHANGED
|
@@ -24,9 +24,19 @@
|
|
|
24
24
|
* cannot drive. Capture is the only path, which is why a failed capture is a
|
|
25
25
|
* hard error with actionable guidance rather than a fallback.
|
|
26
26
|
*/
|
|
27
|
-
import {
|
|
27
|
+
import { captureBearerHeader } from '@chrischall/mcp-utils/fetchproxy';
|
|
28
28
|
import { readPortEnv, readTtlMsEnv } from '@chrischall/mcp-utils';
|
|
29
29
|
import { PACKAGE_NAME, VERSION } from './version.js';
|
|
30
|
+
/**
|
|
31
|
+
* `raceCaptures` settles the per-host attempts under ONE overall deadline:
|
|
32
|
+
* `Promise.any` alone is not enough, because the bridge serializes the two
|
|
33
|
+
* hosts behind its single connection, so the wait was the sum rather than the
|
|
34
|
+
* max — measured live at 14.4s for a 5s window and 36s for a 20s one. At the
|
|
35
|
+
* 30s default that lands past the 60s request timeout an MCP client uses by
|
|
36
|
+
* default. Re-exported (it is mcp-utils', lifted from here) for the test that
|
|
37
|
+
* pins that bound.
|
|
38
|
+
*/
|
|
39
|
+
export { raceCaptures, stripBearer } from '@chrischall/mcp-utils/fetchproxy';
|
|
30
40
|
/**
|
|
31
41
|
* The fetchproxy concentrator port.
|
|
32
42
|
*
|
|
@@ -87,81 +97,6 @@ export const TRUST_DOMAINS = [...CAPTURE_HOSTS];
|
|
|
87
97
|
* enough that an idle tab does not hang a tool call for minutes.
|
|
88
98
|
*/
|
|
89
99
|
const CAPTURE_TIMEOUT_MS = readTtlMsEnv('OUTLOOK_CAPTURE_TIMEOUT', 30_000);
|
|
90
|
-
/**
|
|
91
|
-
* The transport-wide deadline, derived from the capture window rather than left
|
|
92
|
-
* at the library default.
|
|
93
|
-
*
|
|
94
|
-
* `captureRequestHeader`'s own `timeoutMs` is silently capped by
|
|
95
|
-
* `fetchTimeoutMs`: a window raised past the deadline fails early at the
|
|
96
|
-
* deadline's number, which reads as a timeout nobody configured. Deriving it
|
|
97
|
-
* means the two cannot disagree.
|
|
98
|
-
*/
|
|
99
|
-
const BRIDGE_DEADLINE_MS = Math.max(CAPTURE_TIMEOUT_MS + 15_000, 30_000);
|
|
100
|
-
/**
|
|
101
|
-
* Shortest plausible token. Real ones are ~5 KB of JWT; this only guards
|
|
102
|
-
* against an empty or echoed header, not against a wrong-audience token.
|
|
103
|
-
*/
|
|
104
|
-
const MIN_TOKEN_LENGTH = 100;
|
|
105
|
-
/**
|
|
106
|
-
* Add `fetch` to the derived capability set.
|
|
107
|
-
*
|
|
108
|
-
* `createBootstrapOpts` infers capabilities from the bootstrap DECLARATIONS and
|
|
109
|
-
* the field REPLACES the server's defaults rather than extending them, so a
|
|
110
|
-
* declaration-only set can silently lose `fetch`.
|
|
111
|
-
*/
|
|
112
|
-
function withFetch(opts) {
|
|
113
|
-
const derived = opts.capabilities ?? [];
|
|
114
|
-
return {
|
|
115
|
-
...opts,
|
|
116
|
-
capabilities: derived.includes('fetch')
|
|
117
|
-
? derived
|
|
118
|
-
: [...derived, 'fetch'],
|
|
119
|
-
};
|
|
120
|
-
}
|
|
121
|
-
/**
|
|
122
|
-
* Settle a set of per-host capture attempts under ONE overall deadline.
|
|
123
|
-
*
|
|
124
|
-
* `Promise.any` alone is not enough. The two declared hosts are documented as
|
|
125
|
-
* raced, but the bridge serializes them behind its single connection, so the
|
|
126
|
-
* wait was the sum rather than the max — measured live at 14.4s for a 5s window
|
|
127
|
-
* and 36s for a 20s one. At the 30s default that lands past the 60s request
|
|
128
|
-
* timeout an MCP client uses by default, so the host gave up before this code
|
|
129
|
-
* could report the real reason. The window is a promise about total wait, and
|
|
130
|
-
* this is what keeps it.
|
|
131
|
-
*
|
|
132
|
-
* Exported for the test that pins that bound.
|
|
133
|
-
*/
|
|
134
|
-
export async function raceCaptures(attempts, windowMs) {
|
|
135
|
-
// A rejection that loses the race must not surface as an unhandled rejection
|
|
136
|
-
// once the winner has already settled the caller.
|
|
137
|
-
for (const a of attempts)
|
|
138
|
-
a.catch(() => { });
|
|
139
|
-
let timer;
|
|
140
|
-
const expiry = new Promise((_, reject) => {
|
|
141
|
-
timer = setTimeout(() => reject(new Error(`timed out after ${Math.round(windowMs / 1000)}s waiting for the page to ` +
|
|
142
|
-
'make a request we could read')), windowMs);
|
|
143
|
-
});
|
|
144
|
-
try {
|
|
145
|
-
// `any` resolves on the first SUCCESS rather than the first settle, so the
|
|
146
|
-
// host the user is not on failing does not sink the one they are on.
|
|
147
|
-
return await Promise.race([
|
|
148
|
-
Promise.any(attempts).catch((e) => {
|
|
149
|
-
throw new Error(e instanceof AggregateError
|
|
150
|
-
? e.errors.map((x) => x.message).join('; ')
|
|
151
|
-
: e.message);
|
|
152
|
-
}),
|
|
153
|
-
expiry,
|
|
154
|
-
]);
|
|
155
|
-
}
|
|
156
|
-
finally {
|
|
157
|
-
clearTimeout(timer);
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
/** Normalise a captured header to a bare token, dropping any `Bearer ` prefix. */
|
|
161
|
-
export function stripBearer(raw) {
|
|
162
|
-
const m = /^\s*Bearer\s+(.+)$/i.exec(raw);
|
|
163
|
-
return (m ? m[1] : raw).trim();
|
|
164
|
-
}
|
|
165
100
|
/**
|
|
166
101
|
* Capture an Outlook access token from the user's signed-in browser tab.
|
|
167
102
|
*
|
|
@@ -171,54 +106,23 @@ export function stripBearer(raw) {
|
|
|
171
106
|
* Returns the bare token (no `Bearer ` prefix).
|
|
172
107
|
*/
|
|
173
108
|
export async function captureTokenViaFetchproxy() {
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
109
|
+
// `captureBearerHeader` declares only `capture_request_header` beside the
|
|
110
|
+
// `fetch` base verb, derives the bridge deadline from the window (window +
|
|
111
|
+
// 15 s, never under 30 s) so a raised window is not cut off early, starts the
|
|
112
|
+
// transport (skipping `start()` makes the first verb throw "ensureConnected
|
|
113
|
+
// called before listen()", which reads like a pairing problem), rejects a
|
|
114
|
+
// header too short to be a token (real ones are ~5 KB of JWT), and closes
|
|
115
|
+
// the transport whatever happened.
|
|
116
|
+
return captureBearerHeader({
|
|
117
|
+
captures: [CAPTURE_DECL_CLOUD, CAPTURE_DECL_OFFICE],
|
|
118
|
+
domains: TRUST_DOMAINS,
|
|
119
|
+
windowMs: CAPTURE_TIMEOUT_MS,
|
|
181
120
|
port: getWsPort(),
|
|
182
|
-
fetchTimeoutMs: BRIDGE_DEADLINE_MS,
|
|
183
121
|
serverName: PACKAGE_NAME,
|
|
184
122
|
version: VERSION,
|
|
123
|
+
what: 'an Outlook token',
|
|
124
|
+
guidance: 'Open a signed-in Outlook tab (outlook.cloud.microsoft or ' +
|
|
125
|
+
'outlook.office.com), leave it in the foreground for a moment so the ' +
|
|
126
|
+
'page makes a request, and retry.',
|
|
185
127
|
});
|
|
186
|
-
try {
|
|
187
|
-
// `start()` loads the identity keypair. It does not bind the port or dial —
|
|
188
|
-
// that stays lazy until the first verb — but skipping it makes the first
|
|
189
|
-
// verb throw "ensureConnected called before listen()", which reads exactly
|
|
190
|
-
// like a pairing problem and is not one.
|
|
191
|
-
await transport.start();
|
|
192
|
-
const attempts = [CAPTURE_DECL_CLOUD, CAPTURE_DECL_OFFICE].map((decl) => transport.server
|
|
193
|
-
.captureRequestHeader({ ...decl, timeoutMs: CAPTURE_TIMEOUT_MS })
|
|
194
|
-
.then((raw) => {
|
|
195
|
-
if (typeof raw !== 'string') {
|
|
196
|
-
throw new Error(`${decl.host}: capture returned no header`);
|
|
197
|
-
}
|
|
198
|
-
const token = stripBearer(raw);
|
|
199
|
-
if (token.length < MIN_TOKEN_LENGTH) {
|
|
200
|
-
throw new Error(`${decl.host}: authorization header was present but too short to be a ` +
|
|
201
|
-
`token (${token.length} chars)`);
|
|
202
|
-
}
|
|
203
|
-
return token;
|
|
204
|
-
}));
|
|
205
|
-
try {
|
|
206
|
-
return await raceCaptures(attempts, CAPTURE_TIMEOUT_MS);
|
|
207
|
-
}
|
|
208
|
-
catch (e) {
|
|
209
|
-
const detail = e.message;
|
|
210
|
-
throw new Error(`could not capture an Outlook token from the browser (${detail}). ` +
|
|
211
|
-
'Open a signed-in Outlook tab (outlook.cloud.microsoft or ' +
|
|
212
|
-
'outlook.office.com), leave it in the foreground for a moment so the ' +
|
|
213
|
-
'page makes a request, and retry.');
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
finally {
|
|
217
|
-
try {
|
|
218
|
-
await transport.close();
|
|
219
|
-
}
|
|
220
|
-
catch {
|
|
221
|
-
/* the token, or the original failure, is what matters */
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
128
|
}
|
package/dist/bundle.js
CHANGED
|
@@ -35421,10 +35421,33 @@ function redactJsonCookie(quote) {
|
|
|
35421
35421
|
function redactSecrets(text) {
|
|
35422
35422
|
return text.replace(BEARER_RE, "$1[REDACTED]").replace(BASIC_AUTH_RE, "$1[REDACTED]").replace(SET_COOKIE_RE, "$1$2=[REDACTED]").replace(COOKIE_HEADER_RE, (_m, prefix, pairs) => `${prefix}${pairs.replace(/=[^;,\s]*/g, "=[REDACTED]")}`).replace(API_KEY_RE, "[REDACTED]").replace(GOOGLE_OAUTH_TOKEN_RE, "[REDACTED]").replace(HEADER_SECRET_RE, "$1[REDACTED]").replace(QUERY_SECRET_RE, "$1[REDACTED]").replace(OAUTH_CODE_RE, "$1[REDACTED]").replace(FORM_PASSWORD_RE, "$1[REDACTED]").replace(AWS_SIGV4_RE, "$1[REDACTED]").replace(JSON_COOKIE_DQ_RE, redactJsonCookie('"')).replace(JSON_COOKIE_SQ_RE, redactJsonCookie("'")).replace(JSON_SECRET_DQ_RE, "$1[REDACTED]$2").replace(JSON_SECRET_SQ_RE, "$1[REDACTED]$2").replace(JSON_SECRET_NUM_RE, "$1[REDACTED]").replace(JWT_RE, "[REDACTED]");
|
|
35423
35423
|
}
|
|
35424
|
-
function
|
|
35424
|
+
function redactValues(text, values, opts = {}) {
|
|
35425
|
+
const placeholder = opts.placeholder ?? "[REDACTED]";
|
|
35426
|
+
const minLength = Math.max(1, opts.minLength ?? 1);
|
|
35427
|
+
const needles = /* @__PURE__ */ new Set();
|
|
35428
|
+
for (const value of values) {
|
|
35429
|
+
if (typeof value !== "string" || value.length < minLength)
|
|
35430
|
+
continue;
|
|
35431
|
+
needles.add(value);
|
|
35432
|
+
if (opts.jsonEscaped !== false) {
|
|
35433
|
+
const escaped = JSON.stringify(value).slice(1, -1);
|
|
35434
|
+
if (escaped !== value && escaped.length >= minLength)
|
|
35435
|
+
needles.add(escaped);
|
|
35436
|
+
}
|
|
35437
|
+
}
|
|
35438
|
+
let out = String(text ?? "");
|
|
35439
|
+
for (const needle of [...needles].sort((a, b) => b.length - a.length)) {
|
|
35440
|
+
if (out.includes(needle))
|
|
35441
|
+
out = out.split(needle).join(placeholder);
|
|
35442
|
+
}
|
|
35443
|
+
return out;
|
|
35444
|
+
}
|
|
35445
|
+
function truncateErrorMessage(text, max = DEFAULT_ERROR_MESSAGE_MAX, opts = {}) {
|
|
35425
35446
|
const str = text === null || text === void 0 ? "" : String(text);
|
|
35426
35447
|
const cap = Math.max(ERROR_REDACTION_INPUT_MAX, max);
|
|
35427
|
-
const
|
|
35448
|
+
const capped = str.length > cap ? str.slice(0, cap) : str;
|
|
35449
|
+
const scrubbed = opts.values !== void 0 ? redactValues(capped, opts.values, opts.placeholder !== void 0 ? { placeholder: opts.placeholder } : {}) : capped;
|
|
35450
|
+
const redacted = redactSecrets(scrubbed);
|
|
35428
35451
|
if (redacted.length <= max)
|
|
35429
35452
|
return redacted;
|
|
35430
35453
|
return `${redacted.slice(0, max)}\u2026 [truncated]`;
|
|
@@ -35573,6 +35596,16 @@ var init_zod = __esm({
|
|
|
35573
35596
|
}
|
|
35574
35597
|
});
|
|
35575
35598
|
|
|
35599
|
+
// node_modules/@chrischall/mcp-utils/dist/response/media.js
|
|
35600
|
+
var MEDIA_NOUN, MEDIA_QUALIFIER, MEDIA_KEY;
|
|
35601
|
+
var init_media = __esm({
|
|
35602
|
+
"node_modules/@chrischall/mcp-utils/dist/response/media.js"() {
|
|
35603
|
+
MEDIA_NOUN = "(?:avatar|picture|photo|thumbnail|thumb|image|icon|banner|profile_pic(?:ture)?|logo)";
|
|
35604
|
+
MEDIA_QUALIFIER = "(?:primary|secondary|main|default|cover|hero|profile|master|rendered|small|medium|large|full|original|tall)";
|
|
35605
|
+
MEDIA_KEY = new RegExp(`^(?:(?:${MEDIA_QUALIFIER}|${MEDIA_NOUN})[_-]?)?${MEDIA_NOUN}s?(?:[_-]?(?:link|uri|url|src)s?)?$`, "i");
|
|
35606
|
+
}
|
|
35607
|
+
});
|
|
35608
|
+
|
|
35576
35609
|
// node_modules/@chrischall/mcp-utils/dist/response/view.js
|
|
35577
35610
|
function viewParam(honoured, opts = {}) {
|
|
35578
35611
|
if (honoured.length < 2) {
|
|
@@ -35611,6 +35644,7 @@ var VIEWS, DEFAULT_VIEW, BLURB;
|
|
|
35611
35644
|
var init_view = __esm({
|
|
35612
35645
|
"node_modules/@chrischall/mcp-utils/dist/response/view.js"() {
|
|
35613
35646
|
init_zod();
|
|
35647
|
+
init_media();
|
|
35614
35648
|
VIEWS = ["compact", "full", "raw"];
|
|
35615
35649
|
DEFAULT_VIEW = "compact";
|
|
35616
35650
|
BLURB = {
|
|
@@ -35621,16 +35655,6 @@ var init_view = __esm({
|
|
|
35621
35655
|
}
|
|
35622
35656
|
});
|
|
35623
35657
|
|
|
35624
|
-
// node_modules/@chrischall/mcp-utils/dist/response/media.js
|
|
35625
|
-
var MEDIA_NOUN, MEDIA_QUALIFIER, MEDIA_KEY;
|
|
35626
|
-
var init_media = __esm({
|
|
35627
|
-
"node_modules/@chrischall/mcp-utils/dist/response/media.js"() {
|
|
35628
|
-
MEDIA_NOUN = "(?:avatar|picture|photo|thumbnail|thumb|image|icon|banner|profile_pic(?:ture)?|logo)";
|
|
35629
|
-
MEDIA_QUALIFIER = "(?:primary|secondary|main|default|cover|hero|profile|master|rendered|small|medium|large|full|original|tall)";
|
|
35630
|
-
MEDIA_KEY = new RegExp(`^(?:(?:${MEDIA_QUALIFIER}|${MEDIA_NOUN})[_-]?)?${MEDIA_NOUN}s?(?:[_-]?(?:link|uri|url|src)s?)?$`, "i");
|
|
35631
|
-
}
|
|
35632
|
-
});
|
|
35633
|
-
|
|
35634
35658
|
// node_modules/@chrischall/mcp-utils/dist/response/untrusted.js
|
|
35635
35659
|
function isPlainObject3(value) {
|
|
35636
35660
|
if (value === null || typeof value !== "object" || Array.isArray(value))
|
|
@@ -36892,6 +36916,126 @@ var init_url_safety = __esm({
|
|
|
36892
36916
|
}
|
|
36893
36917
|
});
|
|
36894
36918
|
|
|
36919
|
+
// node_modules/@chrischall/mcp-utils/dist/http/transport-failure.js
|
|
36920
|
+
function codeOf(e) {
|
|
36921
|
+
if (e === null || typeof e !== "object")
|
|
36922
|
+
return void 0;
|
|
36923
|
+
const code2 = e.code;
|
|
36924
|
+
return typeof code2 === "string" && code2 !== "" ? code2 : void 0;
|
|
36925
|
+
}
|
|
36926
|
+
function causes(err) {
|
|
36927
|
+
const out = [];
|
|
36928
|
+
let at = err;
|
|
36929
|
+
for (let depth = 0; depth < MAX_CAUSE_DEPTH && at !== void 0 && at !== null; depth++) {
|
|
36930
|
+
out.push(at);
|
|
36931
|
+
if (at instanceof AggregateError)
|
|
36932
|
+
out.push(...at.errors);
|
|
36933
|
+
at = at.cause;
|
|
36934
|
+
}
|
|
36935
|
+
return out;
|
|
36936
|
+
}
|
|
36937
|
+
function transportCauseCode(err) {
|
|
36938
|
+
for (const e of causes(err)) {
|
|
36939
|
+
const code2 = codeOf(e);
|
|
36940
|
+
if (code2 !== void 0)
|
|
36941
|
+
return code2;
|
|
36942
|
+
}
|
|
36943
|
+
return void 0;
|
|
36944
|
+
}
|
|
36945
|
+
function causeMessage(err) {
|
|
36946
|
+
const chain = causes(err).slice(1);
|
|
36947
|
+
for (let i = chain.length - 1; i >= 0; i--) {
|
|
36948
|
+
const e = chain[i];
|
|
36949
|
+
if (e instanceof Error && e.message !== "")
|
|
36950
|
+
return e.message;
|
|
36951
|
+
}
|
|
36952
|
+
return void 0;
|
|
36953
|
+
}
|
|
36954
|
+
function isFetchFailedError(err) {
|
|
36955
|
+
return err instanceof TypeError && err.message === "fetch failed";
|
|
36956
|
+
}
|
|
36957
|
+
function proxyRefusalStatus(err) {
|
|
36958
|
+
for (const e of causes(err)) {
|
|
36959
|
+
if (codeOf(e) === "ERR_PROXY_TUNNEL") {
|
|
36960
|
+
const m = /\b(\d{3})\b/.exec(e instanceof Error ? e.message.slice(0, 500) : "");
|
|
36961
|
+
return m ? Number(m[1]) : "unknown";
|
|
36962
|
+
}
|
|
36963
|
+
if (e instanceof Error) {
|
|
36964
|
+
const m = /^Proxy response \((\d{3})\) !== 200 when HTTP Tunneling/.exec(e.message);
|
|
36965
|
+
if (m)
|
|
36966
|
+
return Number(m[1]);
|
|
36967
|
+
}
|
|
36968
|
+
}
|
|
36969
|
+
return void 0;
|
|
36970
|
+
}
|
|
36971
|
+
function classify(err, code2) {
|
|
36972
|
+
const refused = proxyRefusalStatus(err);
|
|
36973
|
+
if (refused !== void 0) {
|
|
36974
|
+
return { cls: refused === 403 || refused === 407 || refused === "unknown" ? "egress" : "proxy", proxyStatus: refused };
|
|
36975
|
+
}
|
|
36976
|
+
if (code2 !== void 0 && DNS_CODES.has(code2))
|
|
36977
|
+
return { cls: "dns" };
|
|
36978
|
+
if (code2 !== void 0 && CERT_CODES.has(code2))
|
|
36979
|
+
return { cls: "tls" };
|
|
36980
|
+
if (code2 !== void 0 && CONNECT_CODES.has(code2))
|
|
36981
|
+
return { cls: "connect" };
|
|
36982
|
+
return { cls: "other" };
|
|
36983
|
+
}
|
|
36984
|
+
function describeTransportFailure(err, url2, service, opts = {}) {
|
|
36985
|
+
if (!isFetchFailedError(err))
|
|
36986
|
+
return void 0;
|
|
36987
|
+
let host;
|
|
36988
|
+
try {
|
|
36989
|
+
host = new URL(url2).hostname;
|
|
36990
|
+
} catch {
|
|
36991
|
+
host = "(unknown host)";
|
|
36992
|
+
}
|
|
36993
|
+
const code2 = transportCauseCode(err);
|
|
36994
|
+
const detail = [code2, causeMessage(err)].filter((s) => s !== void 0);
|
|
36995
|
+
const why = detail.length > 0 ? ` (${[...new Set(detail)].join(": ")})` : "";
|
|
36996
|
+
const { cls, proxyStatus } = classify(err, code2);
|
|
36997
|
+
const ctx = { host, service, code: code2, ...proxyStatus !== void 0 ? { proxyStatus } : {} };
|
|
36998
|
+
const hint = (opts.hints?.[cls] ?? DEFAULT_HINTS[cls])(host, ctx);
|
|
36999
|
+
return new McpToolError(`Could not reach ${service} at ${host}${why}.`, { kind: "transport", hint, cause: err });
|
|
37000
|
+
}
|
|
37001
|
+
var MAX_CAUSE_DEPTH, DNS_CODES, CONNECT_CODES, CERT_CODES, DEFAULT_HINTS;
|
|
37002
|
+
var init_transport_failure = __esm({
|
|
37003
|
+
"node_modules/@chrischall/mcp-utils/dist/http/transport-failure.js"() {
|
|
37004
|
+
init_errors3();
|
|
37005
|
+
MAX_CAUSE_DEPTH = 5;
|
|
37006
|
+
DNS_CODES = /* @__PURE__ */ new Set(["ENOTFOUND", "EAI_AGAIN", "EAI_FAIL", "EAI_NONAME"]);
|
|
37007
|
+
CONNECT_CODES = /* @__PURE__ */ new Set([
|
|
37008
|
+
"ECONNREFUSED",
|
|
37009
|
+
"ECONNRESET",
|
|
37010
|
+
"ETIMEDOUT",
|
|
37011
|
+
"ENETUNREACH",
|
|
37012
|
+
"EHOSTUNREACH",
|
|
37013
|
+
"EPIPE",
|
|
37014
|
+
"UND_ERR_CONNECT_TIMEOUT",
|
|
37015
|
+
"UND_ERR_SOCKET",
|
|
37016
|
+
"UND_ERR_HEADERS_TIMEOUT"
|
|
37017
|
+
]);
|
|
37018
|
+
CERT_CODES = /* @__PURE__ */ new Set([
|
|
37019
|
+
"UNABLE_TO_GET_ISSUER_CERT_LOCALLY",
|
|
37020
|
+
"UNABLE_TO_GET_ISSUER_CERT",
|
|
37021
|
+
"UNABLE_TO_VERIFY_LEAF_SIGNATURE",
|
|
37022
|
+
"SELF_SIGNED_CERT_IN_CHAIN",
|
|
37023
|
+
"DEPTH_ZERO_SELF_SIGNED_CERT",
|
|
37024
|
+
"CERT_HAS_EXPIRED",
|
|
37025
|
+
"CERT_NOT_YET_VALID",
|
|
37026
|
+
"ERR_TLS_CERT_ALTNAME_INVALID"
|
|
37027
|
+
]);
|
|
37028
|
+
DEFAULT_HINTS = {
|
|
37029
|
+
egress: (host) => `The outbound proxy this server runs behind refused the connection to ${host}: its egress policy does not allow that host. On a hosted (mcp-host) registration, the host must be on the registration's egress allow-list \u2014 the owner accepts the manifest's egress proposal (or adds the host) on the registration, then retries. Elsewhere, ask whoever runs the proxy to allow it.`,
|
|
37030
|
+
proxy: (host, ctx) => `The outbound proxy this server runs behind could not open a connection to ${host} (it answered ${String(ctx.proxyStatus)}). Retry; if it persists, check the proxy can reach the host.`,
|
|
37031
|
+
dns: (host) => `This machine could not resolve ${host}. Check DNS and VPN: a split-tunnel VPN that resolves some of a service's hosts but not others fails exactly like this. \`nslookup ${host}\` on the machine running this server tells you which.`,
|
|
37032
|
+
tls: (host) => `Node did not trust the TLS certificate ${host} presented. On a corporate network this is usually TLS inspection: export your proxy's root CA as PEM and start this server with NODE_EXTRA_CA_CERTS=/path/to/ca.pem (Node does not read the system keychain). If the certificate is expired or not yet valid, also check the machine's clock.`,
|
|
37033
|
+
connect: (host, ctx) => `The connection to ${host} was refused, reset or timed out before ${ctx.service} answered \u2014 usually a firewall, or a network that only lets traffic out through a proxy. Node's fetch ignores proxy settings unless told: start this server with HTTPS_PROXY=<your proxy URL> and NODE_USE_ENV_PROXY=1 (Node 22.21+ / 24.5+), or allow the host through the firewall.`,
|
|
37034
|
+
other: (host) => `No response from ${host}. Check this machine can reach it (VPN, proxy, firewall); \`curl -sI https://${host}/\` from the same machine is a quick test.`
|
|
37035
|
+
};
|
|
37036
|
+
}
|
|
37037
|
+
});
|
|
37038
|
+
|
|
36895
37039
|
// node_modules/@chrischall/mcp-utils/dist/internal/retry-after.js
|
|
36896
37040
|
function retryAfterToMs(header, now = Date.now()) {
|
|
36897
37041
|
if (header == null)
|
|
@@ -36941,6 +37085,27 @@ function cancelBody(res) {
|
|
|
36941
37085
|
} catch {
|
|
36942
37086
|
}
|
|
36943
37087
|
}
|
|
37088
|
+
function writeOutcomeStatus(err) {
|
|
37089
|
+
const status = err.cause?.status;
|
|
37090
|
+
return typeof status === "number" ? status : void 0;
|
|
37091
|
+
}
|
|
37092
|
+
function unknownOutcomeReason(err, opts = {}) {
|
|
37093
|
+
if (err.timedOut)
|
|
37094
|
+
return "it did not answer in time";
|
|
37095
|
+
const status = writeOutcomeStatus(err);
|
|
37096
|
+
if (status !== void 0)
|
|
37097
|
+
return `it answered HTTP ${status} after ${opts.sent ?? "the request"} was sent`;
|
|
37098
|
+
return "the connection dropped";
|
|
37099
|
+
}
|
|
37100
|
+
async function writeOrUnknown(run, onUnknown) {
|
|
37101
|
+
try {
|
|
37102
|
+
return { ok: true, value: await run() };
|
|
37103
|
+
} catch (err) {
|
|
37104
|
+
if (!(err instanceof WriteOutcomeUnknownError))
|
|
37105
|
+
throw err;
|
|
37106
|
+
return { ok: false, result: await onUnknown(err, unknownOutcomeReason(err)) };
|
|
37107
|
+
}
|
|
37108
|
+
}
|
|
36944
37109
|
function headerOf(headers, name) {
|
|
36945
37110
|
if (headers === void 0)
|
|
36946
37111
|
return void 0;
|
|
@@ -37209,7 +37374,7 @@ function createApiClient(opts) {
|
|
|
37209
37374
|
const headers = {
|
|
37210
37375
|
Accept: "application/json",
|
|
37211
37376
|
...withBody && contentType !== void 0 ? { "Content-Type": contentType } : {},
|
|
37212
|
-
...opts.baseHeaders,
|
|
37377
|
+
...typeof opts.baseHeaders === "function" ? opts.baseHeaders() : opts.baseHeaders,
|
|
37213
37378
|
...authHeader(token || void 0),
|
|
37214
37379
|
...opt.headers
|
|
37215
37380
|
};
|
|
@@ -37323,24 +37488,62 @@ function createApiClient(opts) {
|
|
|
37323
37488
|
throw err;
|
|
37324
37489
|
}
|
|
37325
37490
|
}
|
|
37326
|
-
|
|
37327
|
-
|
|
37328
|
-
|
|
37491
|
+
function withIdempotency(opt) {
|
|
37492
|
+
const name = opts.idempotencyHeader;
|
|
37493
|
+
if (name === void 0 || name === "")
|
|
37494
|
+
return opt;
|
|
37495
|
+
const lower2 = name.toLowerCase();
|
|
37496
|
+
if (opt.headers && Object.keys(opt.headers).some((k) => k.toLowerCase() === lower2))
|
|
37497
|
+
return opt;
|
|
37498
|
+
const key = opt.idempotencyKey ?? globalThis.crypto.randomUUID();
|
|
37499
|
+
return { ...opt, headers: { ...opt.headers, [name]: key } };
|
|
37500
|
+
}
|
|
37501
|
+
async function errorFor(res, text, method, path, retried) {
|
|
37502
|
+
const edge = detectEdgeBlock({ body: text, headers: res.headers, status: res.status });
|
|
37503
|
+
if (edge)
|
|
37504
|
+
return new EdgeBlockedError(res.status, edge.vendor, { service, method, path });
|
|
37505
|
+
if (opts.onErrorResponse) {
|
|
37506
|
+
const decision = await opts.onErrorResponse({ status: res.status, method, path, headers: res.headers, body: text, retried });
|
|
37507
|
+
if (decision instanceof Error)
|
|
37508
|
+
return decision;
|
|
37509
|
+
if (decision === "retry" && !retried)
|
|
37510
|
+
return "retry";
|
|
37511
|
+
}
|
|
37512
|
+
return httpError(res, text, method, path, service);
|
|
37513
|
+
}
|
|
37514
|
+
const fetchJson = (method, path, opt = {}) => {
|
|
37515
|
+
const o = withIdempotency(opt);
|
|
37516
|
+
return guardWrite(method, o, (dispatch) => fetchJsonOnce(method, path, o, dispatch));
|
|
37517
|
+
};
|
|
37518
|
+
const fetchHtml = (method, path, opt = {}) => {
|
|
37519
|
+
const o = withIdempotency(opt);
|
|
37520
|
+
return guardWrite(method, o, (dispatch) => fetchHtmlOnce(method, path, o, dispatch));
|
|
37521
|
+
};
|
|
37522
|
+
const fetchRaw = (method, path, opt = {}) => {
|
|
37523
|
+
const o = withIdempotency(opt);
|
|
37524
|
+
return guardWrite(method, o, (dispatch) => fetchRawOnce(method, path, o, dispatch));
|
|
37525
|
+
};
|
|
37329
37526
|
async function fetchJsonOnce(method, path, opt, dispatch) {
|
|
37330
37527
|
const maxBytes = capOf(opt);
|
|
37331
|
-
|
|
37332
|
-
|
|
37333
|
-
|
|
37334
|
-
|
|
37335
|
-
|
|
37336
|
-
|
|
37337
|
-
|
|
37338
|
-
|
|
37339
|
-
|
|
37340
|
-
|
|
37341
|
-
|
|
37342
|
-
|
|
37528
|
+
let attempt;
|
|
37529
|
+
for (let retried = false; ; retried = true) {
|
|
37530
|
+
attempt = await send(method, path, opt, dispatch);
|
|
37531
|
+
const res2 = attempt.res;
|
|
37532
|
+
if (res2.status === 401)
|
|
37533
|
+
throw await unauthorizedOrEdge(attempt, method, path, maxBytes);
|
|
37534
|
+
if (res2.status === 429)
|
|
37535
|
+
throw await rateLimitedError(attempt, method, path, maxBytes);
|
|
37536
|
+
if (res2.status === 204) {
|
|
37537
|
+
discard(attempt);
|
|
37538
|
+
return parseJsonBody("", { expect: opt.expect, service, method, path, status: 204 });
|
|
37539
|
+
}
|
|
37540
|
+
if (res2.ok)
|
|
37541
|
+
break;
|
|
37542
|
+
const failure2 = await errorFor(res2, await readErrorText(attempt, maxBytes), method, path, retried);
|
|
37543
|
+
if (failure2 !== "retry")
|
|
37544
|
+
throw failure2;
|
|
37343
37545
|
}
|
|
37546
|
+
const res = attempt.res;
|
|
37344
37547
|
const text = await readText(attempt, maxBytes);
|
|
37345
37548
|
return parseJsonBody(text, {
|
|
37346
37549
|
expect: opt.expect,
|
|
@@ -37354,34 +37557,43 @@ function createApiClient(opts) {
|
|
|
37354
37557
|
async function fetchHtmlOnce(method, path, opt, dispatch) {
|
|
37355
37558
|
const maxBytes = capOf(opt);
|
|
37356
37559
|
const headers = { Accept: "text/html,*/*", ...opt.headers };
|
|
37357
|
-
|
|
37358
|
-
|
|
37359
|
-
|
|
37360
|
-
|
|
37361
|
-
|
|
37362
|
-
|
|
37363
|
-
|
|
37364
|
-
|
|
37560
|
+
for (let retried = false; ; retried = true) {
|
|
37561
|
+
const attempt = await send(method, path, { ...opt, headers }, dispatch);
|
|
37562
|
+
const res = attempt.res;
|
|
37563
|
+
if (res.status === 401)
|
|
37564
|
+
throw await unauthorizedOrEdge(attempt, method, path, maxBytes);
|
|
37565
|
+
if (res.status === 429)
|
|
37566
|
+
throw await rateLimitedError(attempt, method, path, maxBytes);
|
|
37567
|
+
if (res.ok)
|
|
37568
|
+
return readText(attempt, maxBytes);
|
|
37569
|
+
const failure2 = await errorFor(res, await readErrorText(attempt, maxBytes), method, path, retried);
|
|
37570
|
+
if (failure2 !== "retry")
|
|
37571
|
+
throw failure2;
|
|
37365
37572
|
}
|
|
37366
|
-
return readText(attempt, maxBytes);
|
|
37367
37573
|
}
|
|
37368
37574
|
async function fetchRawOnce(method, path, opt, dispatch) {
|
|
37369
37575
|
const maxBytes = capOf(opt);
|
|
37370
37576
|
const headers = { Accept: "*/*", ...opt.headers };
|
|
37371
|
-
|
|
37372
|
-
|
|
37373
|
-
|
|
37374
|
-
|
|
37375
|
-
|
|
37376
|
-
|
|
37377
|
-
|
|
37577
|
+
let attempt;
|
|
37578
|
+
for (let retried = false; ; retried = true) {
|
|
37579
|
+
attempt = await send(method, path, { ...opt, headers }, dispatch);
|
|
37580
|
+
const res2 = attempt.res;
|
|
37581
|
+
if (res2.status === 401)
|
|
37582
|
+
throw await unauthorizedOrEdge(attempt, method, path, maxBytes);
|
|
37583
|
+
if (res2.status === 429)
|
|
37584
|
+
throw await rateLimitedError(attempt, method, path, maxBytes);
|
|
37585
|
+
if (res2.ok)
|
|
37586
|
+
break;
|
|
37378
37587
|
const text = await readText(attempt, maxBytes).catch((err) => {
|
|
37379
37588
|
if (err instanceof RequestTimeoutError)
|
|
37380
37589
|
throw err;
|
|
37381
37590
|
return "";
|
|
37382
37591
|
});
|
|
37383
|
-
|
|
37592
|
+
const failure2 = await errorFor(res2, text, method, path, retried);
|
|
37593
|
+
if (failure2 !== "retry")
|
|
37594
|
+
throw failure2;
|
|
37384
37595
|
}
|
|
37596
|
+
const res = attempt.res;
|
|
37385
37597
|
const bytes = await readBytes(attempt, maxBytes);
|
|
37386
37598
|
return {
|
|
37387
37599
|
status: res.status,
|
|
@@ -37462,6 +37674,7 @@ var init_http = __esm({
|
|
|
37462
37674
|
init_net_atoms();
|
|
37463
37675
|
init_fetch_bounded();
|
|
37464
37676
|
init_url_safety();
|
|
37677
|
+
init_transport_failure();
|
|
37465
37678
|
init_body_limit();
|
|
37466
37679
|
init_net_atoms();
|
|
37467
37680
|
init_url_safety();
|
|
@@ -37709,7 +37922,7 @@ var init_version = __esm({
|
|
|
37709
37922
|
"src/version.ts"() {
|
|
37710
37923
|
"use strict";
|
|
37711
37924
|
PACKAGE_NAME = "office-outlook-mcp";
|
|
37712
|
-
VERSION = "0.6.
|
|
37925
|
+
VERSION = "0.6.2";
|
|
37713
37926
|
}
|
|
37714
37927
|
});
|
|
37715
37928
|
|
|
@@ -48055,6 +48268,71 @@ function createBootstrapOpts(args) {
|
|
|
48055
48268
|
...nonEmpty(b.domSelectors) ? { domSelectors: b.domSelectors } : {}
|
|
48056
48269
|
};
|
|
48057
48270
|
}
|
|
48271
|
+
function stripBearer(raw) {
|
|
48272
|
+
const m = /^\s*Bearer\s+(.+)$/i.exec(raw);
|
|
48273
|
+
return (m?.[1] ?? raw).trim();
|
|
48274
|
+
}
|
|
48275
|
+
async function raceCaptures(attempts, windowMs) {
|
|
48276
|
+
for (const a of attempts)
|
|
48277
|
+
a.catch(() => {
|
|
48278
|
+
});
|
|
48279
|
+
let timer;
|
|
48280
|
+
const expiry = new Promise((_resolve, reject) => {
|
|
48281
|
+
timer = setTimeout(() => reject(new Error(`timed out after ${Math.round(windowMs / 1e3)}s waiting for the page to make a request we could read`)), windowMs);
|
|
48282
|
+
});
|
|
48283
|
+
try {
|
|
48284
|
+
return await Promise.race([
|
|
48285
|
+
Promise.any(attempts).catch((e) => {
|
|
48286
|
+
throw new Error(e instanceof AggregateError ? e.errors.map((x) => messageOf(x)).join("; ") : messageOf(e));
|
|
48287
|
+
}),
|
|
48288
|
+
expiry
|
|
48289
|
+
]);
|
|
48290
|
+
} finally {
|
|
48291
|
+
clearTimeout(timer);
|
|
48292
|
+
}
|
|
48293
|
+
}
|
|
48294
|
+
async function captureBearerHeader(opts) {
|
|
48295
|
+
if (opts.captures.length === 0)
|
|
48296
|
+
throw new Error("captureBearerHeader: at least one capture declaration is required.");
|
|
48297
|
+
const minLength = opts.minLength ?? 100;
|
|
48298
|
+
const normalise = opts.normalise ?? stripBearer;
|
|
48299
|
+
const domains = opts.domains ?? [...new Set(opts.captures.map((c) => c.host))];
|
|
48300
|
+
const build2 = opts.createTransport ?? ((o) => createFetchproxyTransport(o));
|
|
48301
|
+
const transport = build2({
|
|
48302
|
+
...opts.transportOptions,
|
|
48303
|
+
...createBootstrapOpts({ domains: [...domains], bootstrap: { captureHeaders: opts.captures.map((c) => ({ ...c })) } }),
|
|
48304
|
+
...opts.port !== void 0 ? { port: opts.port } : {},
|
|
48305
|
+
// Both: the explicit deadline for every bridge version, and the declared
|
|
48306
|
+
// window so the transport's own derivation agrees with it.
|
|
48307
|
+
fetchTimeoutMs: deadlineForCaptureWindow(void 0, opts.windowMs) ?? SERVER_DEFAULT_FETCH_TIMEOUT_MS,
|
|
48308
|
+
captureWindowMs: opts.windowMs,
|
|
48309
|
+
serverName: opts.serverName,
|
|
48310
|
+
version: opts.version
|
|
48311
|
+
});
|
|
48312
|
+
const one = async (decl) => {
|
|
48313
|
+
const raw = await transport.server.captureRequestHeader({ ...decl, timeoutMs: opts.windowMs });
|
|
48314
|
+
if (typeof raw !== "string")
|
|
48315
|
+
throw new Error(`${decl.host}: capture returned no header`);
|
|
48316
|
+
const token = normalise(raw);
|
|
48317
|
+
if (token.length < minLength) {
|
|
48318
|
+
throw new Error(`${decl.host}: ${decl.headerName} header was present but too short to be a token (${token.length} chars)`);
|
|
48319
|
+
}
|
|
48320
|
+
return token;
|
|
48321
|
+
};
|
|
48322
|
+
try {
|
|
48323
|
+
await transport.start();
|
|
48324
|
+
try {
|
|
48325
|
+
return opts.captures.length === 1 ? await one(opts.captures[0]) : await raceCaptures(opts.captures.map(one), opts.windowMs);
|
|
48326
|
+
} catch (e) {
|
|
48327
|
+
throw new Error(`could not capture ${opts.what} from the browser (${messageOf(e)}). ${opts.guidance}`);
|
|
48328
|
+
}
|
|
48329
|
+
} finally {
|
|
48330
|
+
try {
|
|
48331
|
+
await transport.close();
|
|
48332
|
+
} catch {
|
|
48333
|
+
}
|
|
48334
|
+
}
|
|
48335
|
+
}
|
|
48058
48336
|
var PLACEHOLDER_RE2, SERVER_DEFAULT_FETCH_TIMEOUT_MS, CAPTURE_DEADLINE_MARGIN_MS;
|
|
48059
48337
|
var init_fetchproxy = __esm({
|
|
48060
48338
|
"node_modules/@chrischall/mcp-utils/dist/fetchproxy/index.js"() {
|
|
@@ -48083,97 +48361,26 @@ __export(auth_fetchproxy_exports, {
|
|
|
48083
48361
|
function getWsPort() {
|
|
48084
48362
|
return readPortEnv("OUTLOOK_WS_PORT", DEFAULT_WS_PORT);
|
|
48085
48363
|
}
|
|
48086
|
-
function withFetch(opts) {
|
|
48087
|
-
const derived2 = opts.capabilities ?? [];
|
|
48088
|
-
return {
|
|
48089
|
-
...opts,
|
|
48090
|
-
capabilities: derived2.includes("fetch") ? derived2 : [...derived2, "fetch"]
|
|
48091
|
-
};
|
|
48092
|
-
}
|
|
48093
|
-
async function raceCaptures(attempts, windowMs) {
|
|
48094
|
-
for (const a of attempts) a.catch(() => {
|
|
48095
|
-
});
|
|
48096
|
-
let timer;
|
|
48097
|
-
const expiry = new Promise((_, reject) => {
|
|
48098
|
-
timer = setTimeout(
|
|
48099
|
-
() => reject(
|
|
48100
|
-
new Error(
|
|
48101
|
-
`timed out after ${Math.round(windowMs / 1e3)}s waiting for the page to make a request we could read`
|
|
48102
|
-
)
|
|
48103
|
-
),
|
|
48104
|
-
windowMs
|
|
48105
|
-
);
|
|
48106
|
-
});
|
|
48107
|
-
try {
|
|
48108
|
-
return await Promise.race([
|
|
48109
|
-
Promise.any(attempts).catch((e) => {
|
|
48110
|
-
throw new Error(
|
|
48111
|
-
e instanceof AggregateError ? e.errors.map((x) => x.message).join("; ") : e.message
|
|
48112
|
-
);
|
|
48113
|
-
}),
|
|
48114
|
-
expiry
|
|
48115
|
-
]);
|
|
48116
|
-
} finally {
|
|
48117
|
-
clearTimeout(timer);
|
|
48118
|
-
}
|
|
48119
|
-
}
|
|
48120
|
-
function stripBearer(raw) {
|
|
48121
|
-
const m = /^\s*Bearer\s+(.+)$/i.exec(raw);
|
|
48122
|
-
return (m ? m[1] : raw).trim();
|
|
48123
|
-
}
|
|
48124
48364
|
async function captureTokenViaFetchproxy() {
|
|
48125
|
-
|
|
48126
|
-
|
|
48127
|
-
|
|
48128
|
-
|
|
48129
|
-
bootstrap: {
|
|
48130
|
-
captureHeaders: [{ ...CAPTURE_DECL_CLOUD }, { ...CAPTURE_DECL_OFFICE }]
|
|
48131
|
-
}
|
|
48132
|
-
})
|
|
48133
|
-
),
|
|
48365
|
+
return captureBearerHeader({
|
|
48366
|
+
captures: [CAPTURE_DECL_CLOUD, CAPTURE_DECL_OFFICE],
|
|
48367
|
+
domains: TRUST_DOMAINS,
|
|
48368
|
+
windowMs: CAPTURE_TIMEOUT_MS,
|
|
48134
48369
|
port: getWsPort(),
|
|
48135
|
-
fetchTimeoutMs: BRIDGE_DEADLINE_MS,
|
|
48136
48370
|
serverName: PACKAGE_NAME,
|
|
48137
|
-
version: VERSION
|
|
48371
|
+
version: VERSION,
|
|
48372
|
+
what: "an Outlook token",
|
|
48373
|
+
guidance: "Open a signed-in Outlook tab (outlook.cloud.microsoft or outlook.office.com), leave it in the foreground for a moment so the page makes a request, and retry."
|
|
48138
48374
|
});
|
|
48139
|
-
try {
|
|
48140
|
-
await transport.start();
|
|
48141
|
-
const attempts = [CAPTURE_DECL_CLOUD, CAPTURE_DECL_OFFICE].map(
|
|
48142
|
-
(decl) => transport.server.captureRequestHeader({ ...decl, timeoutMs: CAPTURE_TIMEOUT_MS }).then((raw) => {
|
|
48143
|
-
if (typeof raw !== "string") {
|
|
48144
|
-
throw new Error(`${decl.host}: capture returned no header`);
|
|
48145
|
-
}
|
|
48146
|
-
const token = stripBearer(raw);
|
|
48147
|
-
if (token.length < MIN_TOKEN_LENGTH) {
|
|
48148
|
-
throw new Error(
|
|
48149
|
-
`${decl.host}: authorization header was present but too short to be a token (${token.length} chars)`
|
|
48150
|
-
);
|
|
48151
|
-
}
|
|
48152
|
-
return token;
|
|
48153
|
-
})
|
|
48154
|
-
);
|
|
48155
|
-
try {
|
|
48156
|
-
return await raceCaptures(attempts, CAPTURE_TIMEOUT_MS);
|
|
48157
|
-
} catch (e) {
|
|
48158
|
-
const detail = e.message;
|
|
48159
|
-
throw new Error(
|
|
48160
|
-
`could not capture an Outlook token from the browser (${detail}). Open a signed-in Outlook tab (outlook.cloud.microsoft or outlook.office.com), leave it in the foreground for a moment so the page makes a request, and retry.`
|
|
48161
|
-
);
|
|
48162
|
-
}
|
|
48163
|
-
} finally {
|
|
48164
|
-
try {
|
|
48165
|
-
await transport.close();
|
|
48166
|
-
} catch {
|
|
48167
|
-
}
|
|
48168
|
-
}
|
|
48169
48375
|
}
|
|
48170
|
-
var DEFAULT_WS_PORT, CAPTURE_DECL_CLOUD, CAPTURE_DECL_OFFICE, CAPTURE_HOSTS, TRUST_DOMAINS, CAPTURE_TIMEOUT_MS
|
|
48376
|
+
var DEFAULT_WS_PORT, CAPTURE_DECL_CLOUD, CAPTURE_DECL_OFFICE, CAPTURE_HOSTS, TRUST_DOMAINS, CAPTURE_TIMEOUT_MS;
|
|
48171
48377
|
var init_auth_fetchproxy = __esm({
|
|
48172
48378
|
"src/auth-fetchproxy.ts"() {
|
|
48173
48379
|
"use strict";
|
|
48174
48380
|
init_fetchproxy();
|
|
48175
48381
|
init_dist2();
|
|
48176
48382
|
init_version();
|
|
48383
|
+
init_fetchproxy();
|
|
48177
48384
|
DEFAULT_WS_PORT = 37149;
|
|
48178
48385
|
CAPTURE_DECL_CLOUD = {
|
|
48179
48386
|
host: "outlook.cloud.microsoft",
|
|
@@ -48188,8 +48395,6 @@ var init_auth_fetchproxy = __esm({
|
|
|
48188
48395
|
CAPTURE_HOSTS = [CAPTURE_DECL_CLOUD.host, CAPTURE_DECL_OFFICE.host];
|
|
48189
48396
|
TRUST_DOMAINS = [...CAPTURE_HOSTS];
|
|
48190
48397
|
CAPTURE_TIMEOUT_MS = readTtlMsEnv("OUTLOOK_CAPTURE_TIMEOUT", 3e4);
|
|
48191
|
-
BRIDGE_DEADLINE_MS = Math.max(CAPTURE_TIMEOUT_MS + 15e3, 3e4);
|
|
48192
|
-
MIN_TOKEN_LENGTH = 100;
|
|
48193
48398
|
}
|
|
48194
48399
|
});
|
|
48195
48400
|
|
|
@@ -48771,6 +48976,46 @@ var TokenManager = class {
|
|
|
48771
48976
|
return res;
|
|
48772
48977
|
}
|
|
48773
48978
|
};
|
|
48979
|
+
var RECAPTURE_SENTINEL = "fetchproxy:recapture";
|
|
48980
|
+
var UNKNOWN_CAPTURED_TOKEN_TTL_MS = 10 * 60 * 1e3;
|
|
48981
|
+
function capturedBearerTokens(accessToken, opts = {}) {
|
|
48982
|
+
const now = opts.now ?? Date.now;
|
|
48983
|
+
let expiresAt;
|
|
48984
|
+
try {
|
|
48985
|
+
expiresAt = decodeJwtExp(accessToken) * 1e3;
|
|
48986
|
+
} catch {
|
|
48987
|
+
expiresAt = now() + (opts.unknownTtlMs ?? UNKNOWN_CAPTURED_TOKEN_TTL_MS);
|
|
48988
|
+
}
|
|
48989
|
+
return { accessToken, refreshToken: RECAPTURE_SENTINEL, expiresAt };
|
|
48990
|
+
}
|
|
48991
|
+
function createCapturedTokenManager(opts) {
|
|
48992
|
+
const { direct } = opts;
|
|
48993
|
+
const wrap = (token) => capturedBearerTokens(token, {
|
|
48994
|
+
...opts.unknownTtlMs !== void 0 ? { unknownTtlMs: opts.unknownTtlMs } : {},
|
|
48995
|
+
...opts.now !== void 0 ? { now: opts.now } : {}
|
|
48996
|
+
});
|
|
48997
|
+
const persistence = direct === void 0 ? opts.persistence ?? void 0 : void 0;
|
|
48998
|
+
return new TokenManager({
|
|
48999
|
+
initial: async () => wrap(direct ?? await opts.capture()),
|
|
49000
|
+
refresh: async () => {
|
|
49001
|
+
if (direct !== void 0) {
|
|
49002
|
+
throw opts.onDirectRejected ? opts.onDirectRejected() : new McpToolError("The directly supplied access token was rejected (401).", {
|
|
49003
|
+
status: 401,
|
|
49004
|
+
kind: "credential_rejected",
|
|
49005
|
+
hint: "A directly supplied token cannot be refreshed automatically. Replace it with a current one, or unset it to capture from a signed-in browser tab instead."
|
|
49006
|
+
});
|
|
49007
|
+
}
|
|
49008
|
+
return wrap(await opts.capture());
|
|
49009
|
+
},
|
|
49010
|
+
...persistence !== void 0 ? { persistence } : {},
|
|
49011
|
+
// Never fatal: the token is in hand and re-capturable.
|
|
49012
|
+
onPersistError: (err) => {
|
|
49013
|
+
opts.onPersistError?.(err);
|
|
49014
|
+
},
|
|
49015
|
+
...opts.skewMs !== void 0 ? { skewMs: opts.skewMs } : {},
|
|
49016
|
+
...opts.now !== void 0 ? { now: opts.now } : {}
|
|
49017
|
+
});
|
|
49018
|
+
}
|
|
48774
49019
|
|
|
48775
49020
|
// src/token-cache.ts
|
|
48776
49021
|
init_dist2();
|
|
@@ -48782,11 +49027,6 @@ function tokenCachePath(env = process.env) {
|
|
|
48782
49027
|
fileName: "token.json"
|
|
48783
49028
|
});
|
|
48784
49029
|
}
|
|
48785
|
-
function isToken(raw) {
|
|
48786
|
-
if (raw === null || typeof raw !== "object") return false;
|
|
48787
|
-
const t = raw;
|
|
48788
|
-
return typeof t.accessToken === "string" && t.accessToken !== "" && typeof t.expiresAt === "number" && (t.refreshToken === void 0 || typeof t.refreshToken === "string");
|
|
48789
|
-
}
|
|
48790
49030
|
function bindingFor(env) {
|
|
48791
49031
|
if (readEnvVar("OUTLOOK_ACCESS_TOKEN", { env }) !== void 0) return null;
|
|
48792
49032
|
if (parseBoolEnv("OUTLOOK_DISABLE_FETCHPROXY", { env })) return null;
|
|
@@ -48799,7 +49039,7 @@ function createTokenCache(env = process.env) {
|
|
|
48799
49039
|
return createFileStatePersistence({
|
|
48800
49040
|
filePath: tokenCachePath(env),
|
|
48801
49041
|
boundTo,
|
|
48802
|
-
validate: (raw) =>
|
|
49042
|
+
validate: (raw) => isBearerTokens(raw) ? raw : null
|
|
48803
49043
|
});
|
|
48804
49044
|
}
|
|
48805
49045
|
function reportCacheWriteFailure(err) {
|
|
@@ -48815,16 +49055,9 @@ async function captureTokenLazily() {
|
|
|
48815
49055
|
return mod.captureTokenViaFetchproxy();
|
|
48816
49056
|
}
|
|
48817
49057
|
var DEFAULT_API_BASE = "https://outlook.office.com/api/v2.0";
|
|
48818
|
-
|
|
48819
|
-
|
|
48820
|
-
|
|
48821
|
-
let expiresAt;
|
|
48822
|
-
try {
|
|
48823
|
-
expiresAt = decodeJwtExp(accessToken) * 1e3;
|
|
48824
|
-
} catch {
|
|
48825
|
-
expiresAt = Date.now() + UNKNOWN_TOKEN_TTL_MS;
|
|
48826
|
-
}
|
|
48827
|
-
return { accessToken, refreshToken: RECAPTURE_SENTINEL, expiresAt };
|
|
49058
|
+
function urlOf(input2) {
|
|
49059
|
+
if (typeof input2 === "string") return input2;
|
|
49060
|
+
return input2 instanceof URL ? input2.href : input2.url;
|
|
48828
49061
|
}
|
|
48829
49062
|
function isCredentialFailure(e) {
|
|
48830
49063
|
const kind = errorKindOf(e);
|
|
@@ -48849,7 +49082,14 @@ var OutlookClient = class {
|
|
|
48849
49082
|
constructor(opts = {}) {
|
|
48850
49083
|
this.#env = opts.env ?? process.env;
|
|
48851
49084
|
this.#capture = opts.captureToken ?? captureTokenLazily;
|
|
48852
|
-
|
|
49085
|
+
const fetchImpl = opts.fetchImpl ?? ((input2, init) => fetch(input2, init));
|
|
49086
|
+
this.#fetchImpl = async (input2, init) => {
|
|
49087
|
+
try {
|
|
49088
|
+
return await fetchImpl(input2, init);
|
|
49089
|
+
} catch (e) {
|
|
49090
|
+
throw describeTransportFailure(e, urlOf(input2), "Outlook") ?? e;
|
|
49091
|
+
}
|
|
49092
|
+
};
|
|
48853
49093
|
this.#baseUrl = readEnvVar("OUTLOOK_API_BASE", { env: this.#env }) ?? DEFAULT_API_BASE;
|
|
48854
49094
|
const direct = readEnvVar("OUTLOOK_ACCESS_TOKEN", { env: this.#env });
|
|
48855
49095
|
const bridgeOff = parseBoolEnv("OUTLOOK_DISABLE_FETCHPROXY", { env: this.#env });
|
|
@@ -48872,51 +49112,17 @@ var OutlookClient = class {
|
|
|
48872
49112
|
if (this.#configError) throw this.#configError;
|
|
48873
49113
|
}
|
|
48874
49114
|
#tokenManager() {
|
|
48875
|
-
if (this.#tokens) return this.#tokens;
|
|
48876
49115
|
const direct = readEnvVar("OUTLOOK_ACCESS_TOKEN", { env: this.#env });
|
|
48877
|
-
|
|
48878
|
-
|
|
48879
|
-
|
|
48880
|
-
|
|
48881
|
-
|
|
48882
|
-
|
|
48883
|
-
|
|
48884
|
-
|
|
48885
|
-
|
|
48886
|
-
|
|
48887
|
-
// and on a machine with no bridge it just hangs for the capture window on
|
|
48888
|
-
// every 401. Say what is wrong instead.
|
|
48889
|
-
refresh: async () => {
|
|
48890
|
-
if (direct !== void 0) {
|
|
48891
|
-
throw new McpToolError("OUTLOOK_ACCESS_TOKEN was rejected by Outlook.", {
|
|
48892
|
-
hint: "A directly-supplied token cannot be refreshed automatically. Replace OUTLOOK_ACCESS_TOKEN with a current one, or unset it to capture from a signed-in Outlook browser tab instead.",
|
|
48893
|
-
status: 401,
|
|
48894
|
-
kind: "credential_rejected"
|
|
48895
|
-
});
|
|
48896
|
-
}
|
|
48897
|
-
return toBearerTokens(await this.#capture());
|
|
48898
|
-
},
|
|
48899
|
-
// A failed cache write must not fail the call that triggered it: the
|
|
48900
|
-
// token is in hand and re-capturable, so a read-only data dir should cost
|
|
48901
|
-
// the next cold start a capture, not this request.
|
|
48902
|
-
...persistence ? {
|
|
48903
|
-
persistence: {
|
|
48904
|
-
load: () => persistence.load(),
|
|
48905
|
-
save: (t) => {
|
|
48906
|
-
try {
|
|
48907
|
-
persistence.save(t);
|
|
48908
|
-
} catch (e) {
|
|
48909
|
-
reportCacheWriteFailure(e);
|
|
48910
|
-
}
|
|
48911
|
-
},
|
|
48912
|
-
clear: () => {
|
|
48913
|
-
try {
|
|
48914
|
-
persistence.clear();
|
|
48915
|
-
} catch {
|
|
48916
|
-
}
|
|
48917
|
-
}
|
|
48918
|
-
}
|
|
48919
|
-
} : {}
|
|
49116
|
+
this.#tokens ??= createCapturedTokenManager({
|
|
49117
|
+
capture: () => this.#capture(),
|
|
49118
|
+
direct,
|
|
49119
|
+
onDirectRejected: () => new McpToolError("OUTLOOK_ACCESS_TOKEN was rejected by Outlook.", {
|
|
49120
|
+
hint: "A directly-supplied token cannot be refreshed automatically. Replace OUTLOOK_ACCESS_TOKEN with a current one, or unset it to capture from a signed-in Outlook browser tab instead.",
|
|
49121
|
+
status: 401,
|
|
49122
|
+
kind: "credential_rejected"
|
|
49123
|
+
}),
|
|
49124
|
+
persistence: direct === void 0 ? createTokenCache(this.#env) : null,
|
|
49125
|
+
onPersistError: reportCacheWriteFailure
|
|
48920
49126
|
});
|
|
48921
49127
|
return this.#tokens;
|
|
48922
49128
|
}
|
|
@@ -50098,19 +50304,14 @@ function dateTimeTimeZone2(value, zone) {
|
|
|
50098
50304
|
return { DateTime: value, TimeZone: zone };
|
|
50099
50305
|
}
|
|
50100
50306
|
var TIMEOUT_SENTENCE = 'If Outlook does not answer in time (or the connection drops) the result has status "unknown": the write may well have gone through, so check before retrying \u2014 never resend blindly.';
|
|
50101
|
-
|
|
50102
|
-
|
|
50103
|
-
|
|
50104
|
-
|
|
50105
|
-
|
|
50106
|
-
|
|
50107
|
-
|
|
50108
|
-
|
|
50109
|
-
status: "unknown",
|
|
50110
|
-
warning: `Outlook did not confirm this write (${e.timedOut ? "it did not answer in time" : "the connection dropped"}), so it may already have happened. Check ${checkWhere} before retrying; do not resend blindly.`
|
|
50111
|
-
})
|
|
50112
|
-
};
|
|
50113
|
-
}
|
|
50307
|
+
function writeOrUnknown2(run, checkWhere) {
|
|
50308
|
+
return writeOrUnknown(
|
|
50309
|
+
run,
|
|
50310
|
+
(e) => minifiedResult({
|
|
50311
|
+
status: "unknown",
|
|
50312
|
+
warning: `Outlook did not confirm this write (${unknownOutcomeReason(e)}), so it may already have happened. Check ${checkWhere} before retrying; do not resend blindly.`
|
|
50313
|
+
})
|
|
50314
|
+
);
|
|
50114
50315
|
}
|
|
50115
50316
|
var INVITE_EVENT_SELECT = "Id,Subject,IsOrganizer,IsCancelled,Start,End,Organizer,ResponseStatus";
|
|
50116
50317
|
var DELETE_EVENT_SELECT = "Id,Subject,IsOrganizer,IsCancelled,ResponseStatus,Type,SeriesMasterId,Start,End,Organizer,Attendees";
|
|
@@ -50218,7 +50419,7 @@ function registerWriteTools(server, client2) {
|
|
|
50218
50419
|
confirmToken
|
|
50219
50420
|
});
|
|
50220
50421
|
if (gate) return gate;
|
|
50221
|
-
const sent = await
|
|
50422
|
+
const sent = await writeOrUnknown2(() => client2.write("POST", "/me/sendmail", payload), "Sent Items");
|
|
50222
50423
|
if (!sent.ok) return sent.result;
|
|
50223
50424
|
return minifiedResult({ sent: true, subject, recipients: recipients || null });
|
|
50224
50425
|
}
|
|
@@ -50244,7 +50445,7 @@ function registerWriteTools(server, client2) {
|
|
|
50244
50445
|
ToRecipients: toRecipients(to),
|
|
50245
50446
|
...cc?.length ? { CcRecipients: toRecipients(cc) } : {}
|
|
50246
50447
|
};
|
|
50247
|
-
const create = await
|
|
50448
|
+
const create = await writeOrUnknown2(
|
|
50248
50449
|
() => client2.write("POST", "/me/messages", payload),
|
|
50249
50450
|
"Drafts"
|
|
50250
50451
|
);
|
|
@@ -50440,7 +50641,7 @@ function registerWriteTools(server, client2) {
|
|
|
50440
50641
|
confirmToken
|
|
50441
50642
|
});
|
|
50442
50643
|
if (gate) return gate;
|
|
50443
|
-
const write = await
|
|
50644
|
+
const write = await writeOrUnknown2(
|
|
50444
50645
|
() => client2.write("POST", "/me/events", payload),
|
|
50445
50646
|
"the calendar (outlook_list_events)"
|
|
50446
50647
|
);
|
|
@@ -50661,7 +50862,7 @@ function registerWriteTools(server, client2) {
|
|
|
50661
50862
|
});
|
|
50662
50863
|
if (gate) return gate;
|
|
50663
50864
|
}
|
|
50664
|
-
const write = await
|
|
50865
|
+
const write = await writeOrUnknown2(
|
|
50665
50866
|
() => client2.write("POST", path, payload),
|
|
50666
50867
|
"the event's response (outlook_get_event)"
|
|
50667
50868
|
);
|
|
@@ -50744,7 +50945,7 @@ function registerWriteTools(server, client2) {
|
|
|
50744
50945
|
});
|
|
50745
50946
|
if (gate) return gate;
|
|
50746
50947
|
}
|
|
50747
|
-
const write = await
|
|
50948
|
+
const write = await writeOrUnknown2(
|
|
50748
50949
|
() => client2.write("DELETE", path),
|
|
50749
50950
|
"the calendar (outlook_get_event)"
|
|
50750
50951
|
);
|
|
@@ -50797,7 +50998,7 @@ function registerWriteTools(server, client2) {
|
|
|
50797
50998
|
const base = `/me/messages/${encodeURIComponent(messageId)}`;
|
|
50798
50999
|
const forwardTo = mode === "forward" ? { ToRecipients: toRecipients(to) } : {};
|
|
50799
51000
|
if (draftOnly === true) {
|
|
50800
|
-
const create = await
|
|
51001
|
+
const create = await writeOrUnknown2(
|
|
50801
51002
|
() => client2.write(
|
|
50802
51003
|
"POST",
|
|
50803
51004
|
`${base}/${draft}`,
|
|
@@ -50864,7 +51065,7 @@ function registerWriteTools(server, client2) {
|
|
|
50864
51065
|
confirmToken
|
|
50865
51066
|
});
|
|
50866
51067
|
if (gate) return gate;
|
|
50867
|
-
const sent = await
|
|
51068
|
+
const sent = await writeOrUnknown2(() => client2.write("POST", path, payload), "Sent Items");
|
|
50868
51069
|
if (!sent.ok) return sent.result;
|
|
50869
51070
|
return mailboxUntrusted({
|
|
50870
51071
|
sent: true,
|
package/dist/client.js
CHANGED
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
* held, every request is a plain Node `fetch` — the bridge is never on the hot
|
|
7
7
|
* path.
|
|
8
8
|
*/
|
|
9
|
-
import { createApiClient,
|
|
10
|
-
import {
|
|
9
|
+
import { createApiClient, describeTransportFailure, errorKindOf, errorStatusOf, McpToolError, parseBoolEnv, readEnvVar, } from '@chrischall/mcp-utils';
|
|
10
|
+
import { createCapturedTokenManager } from '@chrischall/mcp-utils/session';
|
|
11
11
|
import { createTokenCache, reportCacheWriteFailure } from './token-cache.js';
|
|
12
12
|
/**
|
|
13
13
|
* Load the bridge bootstrap lazily.
|
|
@@ -34,35 +34,11 @@ async function captureTokenLazily() {
|
|
|
34
34
|
* is the host that answers. Verified live 2026-09-20.
|
|
35
35
|
*/
|
|
36
36
|
export const DEFAULT_API_BASE = 'https://outlook.office.com/api/v2.0';
|
|
37
|
-
/**
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
* short: being wrong in this direction costs one extra 401-and-replay, while
|
|
43
|
-
* being wrong the other way makes every call fail until a restart.
|
|
44
|
-
*/
|
|
45
|
-
const UNKNOWN_TOKEN_TTL_MS = 10 * 60 * 1000;
|
|
46
|
-
/**
|
|
47
|
-
* Sentinel standing in for a refresh token this flow does not have.
|
|
48
|
-
*
|
|
49
|
-
* Outlook Web's token comes from MSAL's hidden-iframe flow; there is no refresh
|
|
50
|
-
* grant we can drive. "Refreshing" therefore means capturing a new one. The
|
|
51
|
-
* sentinel exists because `TokenManager` only calls `refresh` when a refresh
|
|
52
|
-
* token is present, and without it an expired token would have no path back
|
|
53
|
-
* short of restarting the process.
|
|
54
|
-
*/
|
|
55
|
-
const RECAPTURE_SENTINEL = 'fetchproxy:recapture';
|
|
56
|
-
function toBearerTokens(accessToken) {
|
|
57
|
-
let expiresAt;
|
|
58
|
-
try {
|
|
59
|
-
// decodeJwtExp returns seconds; a non-JWT throws.
|
|
60
|
-
expiresAt = decodeJwtExp(accessToken) * 1000;
|
|
61
|
-
}
|
|
62
|
-
catch {
|
|
63
|
-
expiresAt = Date.now() + UNKNOWN_TOKEN_TTL_MS;
|
|
64
|
-
}
|
|
65
|
-
return { accessToken, refreshToken: RECAPTURE_SENTINEL, expiresAt };
|
|
37
|
+
/** The URL a `fetch` was called with, for naming its host in a transport failure. */
|
|
38
|
+
function urlOf(input) {
|
|
39
|
+
if (typeof input === 'string')
|
|
40
|
+
return input;
|
|
41
|
+
return input instanceof URL ? input.href : input.url;
|
|
66
42
|
}
|
|
67
43
|
/**
|
|
68
44
|
* Whether `e` is about the credential rather than the request: no token
|
|
@@ -97,7 +73,20 @@ export class OutlookClient {
|
|
|
97
73
|
// and calling it as `this.#fetchImpl(...)` binds `this` to the client, and
|
|
98
74
|
// older undici (Node 18-20, which several MCP hosts still bundle) throws
|
|
99
75
|
// `Illegal invocation` for any receiver that is not `globalThis`.
|
|
100
|
-
|
|
76
|
+
const fetchImpl = opts.fetchImpl ?? ((input, init) => fetch(input, init));
|
|
77
|
+
// undici reports every transport failure (DNS, refused/reset connection,
|
|
78
|
+
// untrusted TLS, a proxy refusing the tunnel) as a bare `TypeError('fetch
|
|
79
|
+
// failed')`; name the host, the cause code and what to check instead. The
|
|
80
|
+
// client still marks it a transport failure, so a sent write whose
|
|
81
|
+
// connection drops stays outcome-unknown.
|
|
82
|
+
this.#fetchImpl = async (input, init) => {
|
|
83
|
+
try {
|
|
84
|
+
return await fetchImpl(input, init);
|
|
85
|
+
}
|
|
86
|
+
catch (e) {
|
|
87
|
+
throw describeTransportFailure(e, urlOf(input), 'Outlook') ?? e;
|
|
88
|
+
}
|
|
89
|
+
};
|
|
101
90
|
this.#baseUrl = readEnvVar('OUTLOOK_API_BASE', { env: this.#env }) ?? DEFAULT_API_BASE;
|
|
102
91
|
const direct = readEnvVar('OUTLOOK_ACCESS_TOKEN', { env: this.#env });
|
|
103
92
|
const bridgeOff = parseBoolEnv('OUTLOOK_DISABLE_FETCHPROXY', { env: this.#env });
|
|
@@ -119,59 +108,31 @@ export class OutlookClient {
|
|
|
119
108
|
throw this.#configError;
|
|
120
109
|
}
|
|
121
110
|
#tokenManager() {
|
|
122
|
-
if (this.#tokens)
|
|
123
|
-
return this.#tokens;
|
|
124
111
|
const direct = readEnvVar('OUTLOOK_ACCESS_TOKEN', { env: this.#env });
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
// A failed cache write must not fail the call that triggered it: the
|
|
150
|
-
// token is in hand and re-capturable, so a read-only data dir should cost
|
|
151
|
-
// the next cold start a capture, not this request.
|
|
152
|
-
...(persistence
|
|
153
|
-
? {
|
|
154
|
-
persistence: {
|
|
155
|
-
load: () => persistence.load(),
|
|
156
|
-
save: (t) => {
|
|
157
|
-
try {
|
|
158
|
-
persistence.save(t);
|
|
159
|
-
}
|
|
160
|
-
catch (e) {
|
|
161
|
-
reportCacheWriteFailure(e);
|
|
162
|
-
}
|
|
163
|
-
},
|
|
164
|
-
clear: () => {
|
|
165
|
-
try {
|
|
166
|
-
persistence.clear();
|
|
167
|
-
}
|
|
168
|
-
catch {
|
|
169
|
-
/* nothing useful to do; the next capture overwrites it */
|
|
170
|
-
}
|
|
171
|
-
},
|
|
172
|
-
},
|
|
173
|
-
}
|
|
174
|
-
: {}),
|
|
112
|
+
// Captures on first use (or loads a still-valid cached capture); there is
|
|
113
|
+
// no refresh grant, so a 401 or an expiring token re-captures instead.
|
|
114
|
+
//
|
|
115
|
+
// But a token supplied as OUTLOOK_ACCESS_TOKEN is never cached and never
|
|
116
|
+
// "refreshed" by reaching for the browser: that silently swaps the
|
|
117
|
+
// credential the operator chose for whichever account a tab happens to be
|
|
118
|
+
// signed into, and on a machine with no bridge it just hangs for the
|
|
119
|
+
// capture window on every 401. Say what is wrong instead.
|
|
120
|
+
//
|
|
121
|
+
// A failed cache write must not fail the call that triggered it: the
|
|
122
|
+
// token is in hand and re-capturable, so a read-only data dir should cost
|
|
123
|
+
// the next cold start a capture, not this request.
|
|
124
|
+
this.#tokens ??= createCapturedTokenManager({
|
|
125
|
+
capture: () => this.#capture(),
|
|
126
|
+
direct,
|
|
127
|
+
onDirectRejected: () => new McpToolError('OUTLOOK_ACCESS_TOKEN was rejected by Outlook.', {
|
|
128
|
+
hint: 'A directly-supplied token cannot be refreshed automatically. Replace ' +
|
|
129
|
+
'OUTLOOK_ACCESS_TOKEN with a current one, or unset it to capture from a ' +
|
|
130
|
+
'signed-in Outlook browser tab instead.',
|
|
131
|
+
status: 401,
|
|
132
|
+
kind: 'credential_rejected',
|
|
133
|
+
}),
|
|
134
|
+
persistence: direct === undefined ? createTokenCache(this.#env) : null,
|
|
135
|
+
onPersistError: reportCacheWriteFailure,
|
|
175
136
|
});
|
|
176
137
|
return this.#tokens;
|
|
177
138
|
}
|
package/dist/token-cache.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createFileStatePersistence, resolveStateFile, } from '@chrischall/mcp-utils/session';
|
|
1
|
+
import { createFileStatePersistence, isBearerTokens, resolveStateFile, } from '@chrischall/mcp-utils/session';
|
|
2
2
|
import { readEnvVar, parseBoolEnv } from '@chrischall/mcp-utils';
|
|
3
3
|
/**
|
|
4
4
|
* Where the captured Outlook access token is cached between runs.
|
|
@@ -15,15 +15,6 @@ export function tokenCachePath(env = process.env) {
|
|
|
15
15
|
fileName: 'token.json',
|
|
16
16
|
});
|
|
17
17
|
}
|
|
18
|
-
function isToken(raw) {
|
|
19
|
-
if (raw === null || typeof raw !== 'object')
|
|
20
|
-
return false;
|
|
21
|
-
const t = raw;
|
|
22
|
-
return (typeof t.accessToken === 'string' &&
|
|
23
|
-
t.accessToken !== '' &&
|
|
24
|
-
typeof t.expiresAt === 'number' &&
|
|
25
|
-
(t.refreshToken === undefined || typeof t.refreshToken === 'string'));
|
|
26
|
-
}
|
|
27
18
|
/**
|
|
28
19
|
* What a cached token is bound to, or `null` when there is nothing worth
|
|
29
20
|
* caching.
|
|
@@ -51,7 +42,7 @@ export function createTokenCache(env = process.env) {
|
|
|
51
42
|
return createFileStatePersistence({
|
|
52
43
|
filePath: tokenCachePath(env),
|
|
53
44
|
boundTo,
|
|
54
|
-
validate: (raw) => (
|
|
45
|
+
validate: (raw) => (isBearerTokens(raw) ? raw : null),
|
|
55
46
|
});
|
|
56
47
|
}
|
|
57
48
|
/**
|
package/dist/tools/writes.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import { CONFIRM_FLOW_SENTENCE, CONFIRM_INJECTION_RULE, confirmTokenParam, confirmWrite, errorKindOf, errorStatusOf, mapWithConcurrency, McpToolError, minifiedResult, WriteOutcomeUnknownError, } from '@chrischall/mcp-utils';
|
|
2
|
+
import { CONFIRM_FLOW_SENTENCE, CONFIRM_INJECTION_RULE, confirmTokenParam, confirmWrite, errorKindOf, errorStatusOf, mapWithConcurrency, McpToolError, minifiedResult, unknownOutcomeReason, writeOrUnknown as runWriteOrUnknown, WriteOutcomeUnknownError, } from '@chrischall/mcp-utils';
|
|
3
3
|
import { isCredentialFailure } from '../client.js';
|
|
4
4
|
import { mailboxTimeZone } from '../timezone.js';
|
|
5
5
|
import { cappedRecipients, MAX_RECIPIENTS } from '../view.js';
|
|
@@ -80,21 +80,11 @@ const TIMEOUT_SENTENCE = 'If Outlook does not answer in time (or the connection
|
|
|
80
80
|
* send a second identical email. Any other failure is a real failure and
|
|
81
81
|
* still throws.
|
|
82
82
|
*/
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
if (!(e instanceof WriteOutcomeUnknownError))
|
|
89
|
-
throw e;
|
|
90
|
-
return {
|
|
91
|
-
ok: false,
|
|
92
|
-
result: minifiedResult({
|
|
93
|
-
status: 'unknown',
|
|
94
|
-
warning: `Outlook did not confirm this write (${e.timedOut ? 'it did not answer in time' : 'the connection dropped'}), so it may already have happened. Check ${checkWhere} before retrying; do not resend blindly.`,
|
|
95
|
-
}),
|
|
96
|
-
};
|
|
97
|
-
}
|
|
83
|
+
function writeOrUnknown(run, checkWhere) {
|
|
84
|
+
return runWriteOrUnknown(run, (e) => minifiedResult({
|
|
85
|
+
status: 'unknown',
|
|
86
|
+
warning: `Outlook did not confirm this write (${unknownOutcomeReason(e)}), so it may already have happened. Check ${checkWhere} before retrying; do not resend blindly.`,
|
|
87
|
+
}));
|
|
98
88
|
}
|
|
99
89
|
const INVITE_EVENT_SELECT = 'Id,Subject,IsOrganizer,IsCancelled,Start,End,Organizer,ResponseStatus';
|
|
100
90
|
const DELETE_EVENT_SELECT = 'Id,Subject,IsOrganizer,IsCancelled,ResponseStatus,Type,SeriesMasterId,Start,End,Organizer,Attendees';
|
package/dist/version.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chrischall/office-outlook-mcp",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.2",
|
|
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>",
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"typecheck": "tsc -p tsconfig.check.json"
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
|
-
"@chrischall/mcp-utils": "^3.0
|
|
48
|
+
"@chrischall/mcp-utils": "^3.1.0",
|
|
49
49
|
"@fetchproxy/server": "^3.6.2",
|
|
50
50
|
"@modelcontextprotocol/server": "^2.3.0",
|
|
51
51
|
"dotenv": "^18.0.1",
|
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.6.
|
|
9
|
+
"version": "0.6.2",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "@chrischall/office-outlook-mcp",
|
|
14
|
-
"version": "0.6.
|
|
14
|
+
"version": "0.6.2",
|
|
15
15
|
"transport": {
|
|
16
16
|
"type": "stdio"
|
|
17
17
|
},
|