@chrischall/office-outlook-mcp 0.8.1 → 0.8.3
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/README.md +19 -1
- package/dist/auth-fetchproxy.js +65 -57
- package/dist/bundle.js +63 -49
- package/dist/tools/_paging.js +19 -5
- package/dist/tools/export.js +29 -11
- package/dist/version.js +1 -1
- package/mint.yaml +10 -4
- package/package.json +1 -1
- package/server.json +8 -2
- package/skills/outlook-fpx/SKILL.md +8 -3
|
@@ -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.8.
|
|
10
|
+
"version": "0.8.3"
|
|
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.8.
|
|
18
|
+
"version": "0.8.3",
|
|
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.8.
|
|
4
|
+
"version": "0.8.3",
|
|
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/README.md
CHANGED
|
@@ -54,6 +54,23 @@ npm i -g @fetchproxy/cli
|
|
|
54
54
|
The first capture prints a 6-digit pair code to approve in the ContextMint
|
|
55
55
|
Bridge popup; the grant persists.
|
|
56
56
|
|
|
57
|
+
### Pairing
|
|
58
|
+
|
|
59
|
+
The server declares **one** host to the bridge — `outlook.cloud.microsoft`, or
|
|
60
|
+
`OUTLOOK_WEB_HOST` — and the same scope on every capture, so once approved it
|
|
61
|
+
neither re-pairs nor asks for wider grants. The bridge keeps one relay tab on
|
|
62
|
+
that host open (it reuses yours if you already have Outlook open).
|
|
63
|
+
|
|
64
|
+
Before 0.8.2 it also declared `outlook.office.com`. A signed-in tab there
|
|
65
|
+
redirects to `outlook.cloud.microsoft`, so the bridge opened another background
|
|
66
|
+
tab on every capture. Upgrading changes the declared domain set, so the bridge
|
|
67
|
+
asks to pair **once** more.
|
|
68
|
+
|
|
69
|
+
If you run the bridge in more than one browser or browser profile, each one is a
|
|
70
|
+
different extension identity, and the server trusts only the one it paired
|
|
71
|
+
with. Keep the bridge enabled in the browser you pair with (or re-pair
|
|
72
|
+
deliberately with `FETCHPROXY_TRUST_NEW_EXTENSION=1` after switching browsers).
|
|
73
|
+
|
|
57
74
|
### Install in opencode
|
|
58
75
|
|
|
59
76
|
opencode reads MCP servers from `opencode.json` (project) or
|
|
@@ -100,7 +117,8 @@ Everything is optional — with nothing set, the server captures from the browse
|
|
|
100
117
|
| `OUTLOOK_DISABLE_FETCHPROXY` | `1` to disable browser capture, requiring `OUTLOOK_ACCESS_TOKEN`. |
|
|
101
118
|
| `OUTLOOK_API_BASE` | Override the REST base URL. Defaults to `https://outlook.office.com/api/v2.0`. |
|
|
102
119
|
| `OUTLOOK_WS_PORT` | fetchproxy concentrator port. Defaults to `37149`, the fleet-wide shared port. |
|
|
103
|
-
| `
|
|
120
|
+
| `OUTLOOK_WEB_HOST` | Which Outlook Web host your signed-in tab is on: `outlook.cloud.microsoft` (default) or `outlook.office.com` for a tenant still on the legacy app. Only this one host is declared to the bridge — see [Pairing](#pairing). |
|
|
121
|
+
| `OUTLOOK_CAPTURE_TIMEOUT` | Seconds to wait for the tab to make a readable request. Defaults to `30`. |
|
|
104
122
|
| `OUTLOOK_TOKEN_CACHE` | `false` to stop caching the token between runs. |
|
|
105
123
|
| `OUTLOOK_TOKEN_FILE` | Path to the cached token. Defaults to `~/.office-outlook-mcp/token.json`. |
|
|
106
124
|
| `OUTLOOK_OUTPUT_DIR` | Absolute directory `outlook_export_messages` and `outlook_export_events` write into. Defaults to `~/Downloads/office-outlook-mcp`. When set, a per-call `dest` must be inside it. See [Bulk export](#bulk-export). |
|
package/dist/auth-fetchproxy.js
CHANGED
|
@@ -15,8 +15,9 @@
|
|
|
15
15
|
* domain), but the token it carries has `aud: https://outlook.office.com`;
|
|
16
16
|
* - that token authenticates plain server-side requests to
|
|
17
17
|
* `outlook.office.com/api/v2.0` with no bridge involved;
|
|
18
|
-
* - older tenants still serve the app from `outlook.office.com
|
|
19
|
-
*
|
|
18
|
+
* - older tenants still serve the app from `outlook.office.com`; those set
|
|
19
|
+
* `OUTLOOK_WEB_HOST=outlook.office.com`. Only ONE host is ever declared —
|
|
20
|
+
* see `captureHost` for why.
|
|
20
21
|
*
|
|
21
22
|
* We do NOT try to mint a token by calling an endpoint ourselves. There is no
|
|
22
23
|
* such endpoint reachable from the page's origin — the token comes from MSAL's
|
|
@@ -24,19 +25,10 @@
|
|
|
24
25
|
* cannot drive. Capture is the only path, which is why a failed capture is a
|
|
25
26
|
* hard error with actionable guidance rather than a fallback.
|
|
26
27
|
*/
|
|
27
|
-
import { captureBearerHeader } from '@chrischall/mcp-utils/fetchproxy';
|
|
28
|
-
import { readPortEnv, readTtlMsEnv } from '@chrischall/mcp-utils';
|
|
28
|
+
import { captureBearerHeader, createFetchproxyTransport } from '@chrischall/mcp-utils/fetchproxy';
|
|
29
|
+
import { readEnvVar, readPortEnv, readTtlMsEnv } from '@chrischall/mcp-utils';
|
|
29
30
|
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';
|
|
31
|
+
export { stripBearer } from '@chrischall/mcp-utils/fetchproxy';
|
|
40
32
|
/**
|
|
41
33
|
* The fetchproxy concentrator port.
|
|
42
34
|
*
|
|
@@ -51,42 +43,49 @@ export { raceCaptures, stripBearer } from '@chrischall/mcp-utils/fetchproxy';
|
|
|
51
43
|
* instead of handing `NaN` to the server.
|
|
52
44
|
*/
|
|
53
45
|
const DEFAULT_WS_PORT = 37_149;
|
|
54
|
-
export function getWsPort() {
|
|
55
|
-
return readPortEnv('OUTLOOK_WS_PORT', DEFAULT_WS_PORT);
|
|
46
|
+
export function getWsPort(env = process.env) {
|
|
47
|
+
return readPortEnv('OUTLOOK_WS_PORT', DEFAULT_WS_PORT, { env });
|
|
56
48
|
}
|
|
57
|
-
/** New Outlook Web. This is where a current tenant's tab actually lives. */
|
|
58
|
-
const CAPTURE_DECL_CLOUD = {
|
|
59
|
-
host: 'outlook.cloud.microsoft',
|
|
60
|
-
path: '/*',
|
|
61
|
-
headerName: 'authorization',
|
|
62
|
-
};
|
|
63
|
-
/** Legacy host, still serving the app for tenants not yet migrated. */
|
|
64
|
-
const CAPTURE_DECL_OFFICE = {
|
|
65
|
-
host: 'outlook.office.com',
|
|
66
|
-
path: '/*',
|
|
67
|
-
headerName: 'authorization',
|
|
68
|
-
};
|
|
69
49
|
/**
|
|
70
|
-
* The
|
|
50
|
+
* The two Outlook Web origins a signed-in tab can be on: the new one first,
|
|
51
|
+
* then the legacy one still serving tenants not yet migrated.
|
|
71
52
|
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
53
|
+
* Only one is ever DECLARED to the bridge (`captureHost`). Both stay listed
|
|
54
|
+
* for validating `OUTLOOK_WEB_HOST` and for a hosted registration's egress.
|
|
74
55
|
*/
|
|
75
|
-
export const
|
|
56
|
+
export const OUTLOOK_WEB_HOSTS = ['outlook.cloud.microsoft', 'outlook.office.com'];
|
|
76
57
|
/**
|
|
77
|
-
* The
|
|
58
|
+
* The ONE host to capture from and declare to the bridge: `OUTLOOK_WEB_HOST`,
|
|
59
|
+
* or `outlook.cloud.microsoft` by default.
|
|
78
60
|
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
61
|
+
* This declared both hosts until 0.8.1, and the bridge opens a background
|
|
62
|
+
* relay tab for every declared domain that has no tab open, after every hello.
|
|
63
|
+
* A signed-in `outlook.office.com` tab redirects to `outlook.cloud.microsoft`,
|
|
64
|
+
* so no tab ever matched the second domain and each capture opened another
|
|
65
|
+
* one. The second capture also ran after the first (the bridge serializes
|
|
66
|
+
* them), so a capture from the legacy host waited out the other host's window
|
|
67
|
+
* first.
|
|
83
68
|
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
69
|
+
* Declaring one host fixes both. It is also the smallest scope the bridge can
|
|
70
|
+
* be asked for, and the same on every hello, so it never offers a grant again.
|
|
71
|
+
* Changing the host changes the domain set, which the bridge re-pairs on — a
|
|
72
|
+
* one-time approval.
|
|
73
|
+
*
|
|
74
|
+
* An unrecognised value throws rather than falling back: capturing from the
|
|
75
|
+
* wrong host just times out, with nothing saying why.
|
|
88
76
|
*/
|
|
89
|
-
export
|
|
77
|
+
export function captureHost(env = process.env) {
|
|
78
|
+
const raw = readEnvVar('OUTLOOK_WEB_HOST', { env });
|
|
79
|
+
if (raw === undefined)
|
|
80
|
+
return OUTLOOK_WEB_HOSTS[0];
|
|
81
|
+
const host = raw.trim().toLowerCase();
|
|
82
|
+
const known = OUTLOOK_WEB_HOSTS.find((h) => h === host);
|
|
83
|
+
if (known === undefined) {
|
|
84
|
+
throw new Error(`OUTLOOK_WEB_HOST=${JSON.stringify(raw)} is not an Outlook Web host. Use ` +
|
|
85
|
+
`${OUTLOOK_WEB_HOSTS.join(' or ')} — whichever your signed-in Outlook tab is on — or unset it.`);
|
|
86
|
+
}
|
|
87
|
+
return known;
|
|
88
|
+
}
|
|
90
89
|
/**
|
|
91
90
|
* How long to wait for the page to make a request we can read.
|
|
92
91
|
*
|
|
@@ -96,16 +95,19 @@ export const TRUST_DOMAINS = [...CAPTURE_HOSTS];
|
|
|
96
95
|
* long enough that ordinary polling or a user glance lands inside it, short
|
|
97
96
|
* enough that an idle tab does not hang a tool call for minutes.
|
|
98
97
|
*/
|
|
99
|
-
const
|
|
98
|
+
const DEFAULT_CAPTURE_WINDOW_MS = 30_000;
|
|
100
99
|
/**
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
100
|
+
* Build the capture transport through this module's import binding, so the
|
|
101
|
+
* test can stand in for the bridge. mcp-utils' own default calls its internal
|
|
102
|
+
* reference, which a module mock cannot reach.
|
|
103
|
+
*/
|
|
104
|
+
const createCaptureTransport = (opts) => createFetchproxyTransport(opts);
|
|
105
|
+
/**
|
|
106
|
+
* Capture an Outlook access token from the user's signed-in browser tab on
|
|
107
|
+
* `captureHost(env)`. Returns the bare token (no `Bearer ` prefix).
|
|
107
108
|
*/
|
|
108
|
-
export async function captureTokenViaFetchproxy() {
|
|
109
|
+
export async function captureTokenViaFetchproxy(env = process.env) {
|
|
110
|
+
const host = captureHost(env);
|
|
109
111
|
// `captureBearerHeader` declares only `capture_request_header` beside the
|
|
110
112
|
// `fetch` base verb, derives the bridge deadline from the window (window +
|
|
111
113
|
// 15 s, never under 30 s) so a raised window is not cut off early, starts the
|
|
@@ -114,15 +116,21 @@ export async function captureTokenViaFetchproxy() {
|
|
|
114
116
|
// header too short to be a token (real ones are ~5 KB of JWT), and closes
|
|
115
117
|
// the transport whatever happened.
|
|
116
118
|
return captureBearerHeader({
|
|
117
|
-
captures: [
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
119
|
+
captures: [{ host, path: '/*', headerName: 'authorization' }],
|
|
120
|
+
// Exactly the host captured from: every API call afterwards is a plain
|
|
121
|
+
// server-side `fetch` the bridge never sees. (The apex `cloud.microsoft` /
|
|
122
|
+
// `office.com` once declared here granted the whole of Microsoft 365.)
|
|
123
|
+
domains: [host],
|
|
124
|
+
// Read per capture, not at import, so the env this server was given wins.
|
|
125
|
+
windowMs: readTtlMsEnv('OUTLOOK_CAPTURE_TIMEOUT', DEFAULT_CAPTURE_WINDOW_MS, { env }),
|
|
126
|
+
port: getWsPort(env),
|
|
121
127
|
serverName: PACKAGE_NAME,
|
|
122
128
|
version: VERSION,
|
|
123
129
|
what: 'an Outlook token',
|
|
124
|
-
guidance:
|
|
125
|
-
'
|
|
126
|
-
|
|
130
|
+
guidance: `Open a signed-in Outlook tab on ${host}, leave it in the foreground for a ` +
|
|
131
|
+
'moment so the page makes a request, and retry. If your Outlook is on the ' +
|
|
132
|
+
`other host (${OUTLOOK_WEB_HOSTS.filter((h) => h !== host).join(', ')}), set ` +
|
|
133
|
+
'OUTLOOK_WEB_HOST to it.',
|
|
134
|
+
createTransport: createCaptureTransport,
|
|
127
135
|
});
|
|
128
136
|
}
|
package/dist/bundle.js
CHANGED
|
@@ -39086,7 +39086,7 @@ var init_version = __esm({
|
|
|
39086
39086
|
"src/version.ts"() {
|
|
39087
39087
|
"use strict";
|
|
39088
39088
|
PACKAGE_NAME = "office-outlook-mcp";
|
|
39089
|
-
VERSION = "0.8.
|
|
39089
|
+
VERSION = "0.8.3";
|
|
39090
39090
|
}
|
|
39091
39091
|
});
|
|
39092
39092
|
|
|
@@ -43398,7 +43398,7 @@ var require_websocket = __commonJS({
|
|
|
43398
43398
|
var http = __require("http");
|
|
43399
43399
|
var net = __require("net");
|
|
43400
43400
|
var tls = __require("tls");
|
|
43401
|
-
var { randomBytes: randomBytes8, createHash:
|
|
43401
|
+
var { randomBytes: randomBytes8, createHash: createHash6 } = __require("crypto");
|
|
43402
43402
|
var { Duplex, Readable } = __require("stream");
|
|
43403
43403
|
var { URL: URL2 } = __require("url");
|
|
43404
43404
|
var PerMessageDeflate2 = require_permessage_deflate();
|
|
@@ -44079,7 +44079,7 @@ var require_websocket = __commonJS({
|
|
|
44079
44079
|
abortHandshake(websocket, socket, "Invalid Upgrade header");
|
|
44080
44080
|
return;
|
|
44081
44081
|
}
|
|
44082
|
-
const digest =
|
|
44082
|
+
const digest = createHash6("sha1").update(key + GUID).digest("base64");
|
|
44083
44083
|
if (res.headers["sec-websocket-accept"] !== digest) {
|
|
44084
44084
|
abortHandshake(websocket, socket, "Invalid Sec-WebSocket-Accept header");
|
|
44085
44085
|
return;
|
|
@@ -44448,7 +44448,7 @@ var require_websocket_server = __commonJS({
|
|
|
44448
44448
|
var EventEmitter = __require("events");
|
|
44449
44449
|
var http = __require("http");
|
|
44450
44450
|
var { Duplex } = __require("stream");
|
|
44451
|
-
var { createHash:
|
|
44451
|
+
var { createHash: createHash6 } = __require("crypto");
|
|
44452
44452
|
var extension2 = require_extension();
|
|
44453
44453
|
var PerMessageDeflate2 = require_permessage_deflate();
|
|
44454
44454
|
var subprotocol2 = require_subprotocol();
|
|
@@ -44755,7 +44755,7 @@ var require_websocket_server = __commonJS({
|
|
|
44755
44755
|
);
|
|
44756
44756
|
}
|
|
44757
44757
|
if (this._state > RUNNING) return abortHandshake(socket, 503);
|
|
44758
|
-
const digest =
|
|
44758
|
+
const digest = createHash6("sha1").update(key + GUID).digest("base64");
|
|
44759
44759
|
const headers = [
|
|
44760
44760
|
"HTTP/1.1 101 Switching Protocols",
|
|
44761
44761
|
"Upgrade: websocket",
|
|
@@ -49515,29 +49515,46 @@ var init_fetchproxy = __esm({
|
|
|
49515
49515
|
// src/auth-fetchproxy.ts
|
|
49516
49516
|
var auth_fetchproxy_exports = {};
|
|
49517
49517
|
__export(auth_fetchproxy_exports, {
|
|
49518
|
-
|
|
49519
|
-
|
|
49518
|
+
OUTLOOK_WEB_HOSTS: () => OUTLOOK_WEB_HOSTS,
|
|
49519
|
+
captureHost: () => captureHost,
|
|
49520
49520
|
captureTokenViaFetchproxy: () => captureTokenViaFetchproxy,
|
|
49521
49521
|
getWsPort: () => getWsPort,
|
|
49522
|
-
raceCaptures: () => raceCaptures,
|
|
49523
49522
|
stripBearer: () => stripBearer
|
|
49524
49523
|
});
|
|
49525
|
-
function getWsPort() {
|
|
49526
|
-
return readPortEnv("OUTLOOK_WS_PORT", DEFAULT_WS_PORT);
|
|
49524
|
+
function getWsPort(env = process.env) {
|
|
49525
|
+
return readPortEnv("OUTLOOK_WS_PORT", DEFAULT_WS_PORT, { env });
|
|
49526
|
+
}
|
|
49527
|
+
function captureHost(env = process.env) {
|
|
49528
|
+
const raw = readEnvVar("OUTLOOK_WEB_HOST", { env });
|
|
49529
|
+
if (raw === void 0) return OUTLOOK_WEB_HOSTS[0];
|
|
49530
|
+
const host = raw.trim().toLowerCase();
|
|
49531
|
+
const known = OUTLOOK_WEB_HOSTS.find((h) => h === host);
|
|
49532
|
+
if (known === void 0) {
|
|
49533
|
+
throw new Error(
|
|
49534
|
+
`OUTLOOK_WEB_HOST=${JSON.stringify(raw)} is not an Outlook Web host. Use ${OUTLOOK_WEB_HOSTS.join(" or ")} \u2014 whichever your signed-in Outlook tab is on \u2014 or unset it.`
|
|
49535
|
+
);
|
|
49536
|
+
}
|
|
49537
|
+
return known;
|
|
49527
49538
|
}
|
|
49528
|
-
async function captureTokenViaFetchproxy() {
|
|
49539
|
+
async function captureTokenViaFetchproxy(env = process.env) {
|
|
49540
|
+
const host = captureHost(env);
|
|
49529
49541
|
return captureBearerHeader({
|
|
49530
|
-
captures: [
|
|
49531
|
-
|
|
49532
|
-
|
|
49533
|
-
|
|
49542
|
+
captures: [{ host, path: "/*", headerName: "authorization" }],
|
|
49543
|
+
// Exactly the host captured from: every API call afterwards is a plain
|
|
49544
|
+
// server-side `fetch` the bridge never sees. (The apex `cloud.microsoft` /
|
|
49545
|
+
// `office.com` once declared here granted the whole of Microsoft 365.)
|
|
49546
|
+
domains: [host],
|
|
49547
|
+
// Read per capture, not at import, so the env this server was given wins.
|
|
49548
|
+
windowMs: readTtlMsEnv("OUTLOOK_CAPTURE_TIMEOUT", DEFAULT_CAPTURE_WINDOW_MS, { env }),
|
|
49549
|
+
port: getWsPort(env),
|
|
49534
49550
|
serverName: PACKAGE_NAME,
|
|
49535
49551
|
version: VERSION,
|
|
49536
49552
|
what: "an Outlook token",
|
|
49537
|
-
guidance:
|
|
49553
|
+
guidance: `Open a signed-in Outlook tab on ${host}, leave it in the foreground for a moment so the page makes a request, and retry. If your Outlook is on the other host (${OUTLOOK_WEB_HOSTS.filter((h) => h !== host).join(", ")}), set OUTLOOK_WEB_HOST to it.`,
|
|
49554
|
+
createTransport: createCaptureTransport
|
|
49538
49555
|
});
|
|
49539
49556
|
}
|
|
49540
|
-
var DEFAULT_WS_PORT,
|
|
49557
|
+
var DEFAULT_WS_PORT, OUTLOOK_WEB_HOSTS, DEFAULT_CAPTURE_WINDOW_MS, createCaptureTransport;
|
|
49541
49558
|
var init_auth_fetchproxy = __esm({
|
|
49542
49559
|
"src/auth-fetchproxy.ts"() {
|
|
49543
49560
|
"use strict";
|
|
@@ -49546,19 +49563,9 @@ var init_auth_fetchproxy = __esm({
|
|
|
49546
49563
|
init_version();
|
|
49547
49564
|
init_fetchproxy();
|
|
49548
49565
|
DEFAULT_WS_PORT = 37149;
|
|
49549
|
-
|
|
49550
|
-
|
|
49551
|
-
|
|
49552
|
-
headerName: "authorization"
|
|
49553
|
-
};
|
|
49554
|
-
CAPTURE_DECL_OFFICE = {
|
|
49555
|
-
host: "outlook.office.com",
|
|
49556
|
-
path: "/*",
|
|
49557
|
-
headerName: "authorization"
|
|
49558
|
-
};
|
|
49559
|
-
CAPTURE_HOSTS = [CAPTURE_DECL_CLOUD.host, CAPTURE_DECL_OFFICE.host];
|
|
49560
|
-
TRUST_DOMAINS = [...CAPTURE_HOSTS];
|
|
49561
|
-
CAPTURE_TIMEOUT_MS = readTtlMsEnv("OUTLOOK_CAPTURE_TIMEOUT", 3e4);
|
|
49566
|
+
OUTLOOK_WEB_HOSTS = ["outlook.cloud.microsoft", "outlook.office.com"];
|
|
49567
|
+
DEFAULT_CAPTURE_WINDOW_MS = 3e4;
|
|
49568
|
+
createCaptureTransport = (opts) => createFetchproxyTransport(opts);
|
|
49562
49569
|
}
|
|
49563
49570
|
});
|
|
49564
49571
|
|
|
@@ -50596,6 +50603,7 @@ function isMeetingMessage(m) {
|
|
|
50596
50603
|
// src/tools/_paging.ts
|
|
50597
50604
|
init_zod();
|
|
50598
50605
|
init_dist2();
|
|
50606
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
50599
50607
|
var nextLinkParam = external_exports.string().min(1).optional().describe(
|
|
50600
50608
|
"Continue a previous listing: pass its `nextLink` value unchanged to fetch the next page. When set, the paging/filter arguments are taken from the link and ignored here."
|
|
50601
50609
|
);
|
|
@@ -50611,6 +50619,10 @@ function plainCollection(data) {
|
|
|
50611
50619
|
if (next) out.nextLink = next;
|
|
50612
50620
|
return out;
|
|
50613
50621
|
}
|
|
50622
|
+
var MAX_KEY_LENGTH = 16;
|
|
50623
|
+
function idKey(id) {
|
|
50624
|
+
return id.length <= MAX_KEY_LENGTH ? id : createHash4("sha256").update(id).digest("base64url").slice(0, 12);
|
|
50625
|
+
}
|
|
50614
50626
|
var fromParam = external_exports.string().min(1).optional().describe(
|
|
50615
50627
|
'Page oldest first from this point: an ISO 8601 UTC time (e.g. "2026-10-01T00:00:00Z"; one with no zone is read as UTC), or a previous result\'s `next_from` passed unchanged. The result always carries `next_from` and `has_more`.'
|
|
50616
50628
|
);
|
|
@@ -50656,24 +50668,24 @@ function decodeFrom(from) {
|
|
|
50656
50668
|
if (ms === void 0 || !Array.isArray(p?.ids) || !p.ids.every((id) => typeof id === "string")) {
|
|
50657
50669
|
throw new McpToolError("`from` is neither an ISO 8601 time nor a next_from token.", { hint: FROM_HINT });
|
|
50658
50670
|
}
|
|
50659
|
-
return { t: utcLiteral(ms), ids: p.ids };
|
|
50671
|
+
return { t: utcLiteral(ms), ids: [...new Set(p.ids.map(idKey))] };
|
|
50660
50672
|
}
|
|
50661
50673
|
function fromQuery(pos, limit) {
|
|
50662
50674
|
return { bound: `ReceivedDateTime ge ${pos.t}`, top: Math.min(limit + pos.ids.length, MAX_TOP) };
|
|
50663
50675
|
}
|
|
50664
50676
|
function fromPage(rows, pos, limit, apiHasMore) {
|
|
50665
|
-
const seen = new Set(pos.ids);
|
|
50666
|
-
const fresh = rows.filter((r) => r.Id === void 0 || !seen.has(r.Id));
|
|
50677
|
+
const seen = new Set(pos.ids.map(idKey));
|
|
50678
|
+
const fresh = rows.filter((r) => r.Id === void 0 || !seen.has(idKey(r.Id)));
|
|
50667
50679
|
const kept = fresh.slice(0, limit);
|
|
50668
50680
|
const next = nextPosition(kept, pos);
|
|
50669
50681
|
return { rows: kept, next_from: encodeFrom(next), has_more: fresh.length > limit || apiHasMore };
|
|
50670
50682
|
}
|
|
50671
50683
|
function nextPosition(rows, pos) {
|
|
50672
|
-
const dated = rows.map((r) => ({ id: r.Id, ms: r.ReceivedDateTime ? Date.parse(r.ReceivedDateTime) : NaN })).filter((r) => r.id !== void 0 && !Number.isNaN(r.ms));
|
|
50684
|
+
const dated = rows.map((r) => ({ id: r.Id === void 0 ? void 0 : idKey(r.Id), ms: r.ReceivedDateTime ? Date.parse(r.ReceivedDateTime) : NaN })).filter((r) => r.id !== void 0 && !Number.isNaN(r.ms));
|
|
50673
50685
|
if (dated.length === 0) return pos;
|
|
50674
50686
|
const newest = Math.max(...dated.map((r) => r.ms));
|
|
50675
50687
|
const tied = dated.filter((r) => r.ms === newest).map((r) => r.id);
|
|
50676
|
-
const carried = newest === Date.parse(pos.t) ? pos.ids.filter((id) => !tied.includes(id)) : [];
|
|
50688
|
+
const carried = newest === Date.parse(pos.t) ? pos.ids.map(idKey).filter((id) => !tied.includes(id)) : [];
|
|
50677
50689
|
return { t: utcLiteral(newest), ids: [...carried, ...tied] };
|
|
50678
50690
|
}
|
|
50679
50691
|
|
|
@@ -52374,7 +52386,7 @@ function registerHealthcheckTool(server, client2) {
|
|
|
52374
52386
|
// src/tools/export.ts
|
|
52375
52387
|
init_zod();
|
|
52376
52388
|
init_dist2();
|
|
52377
|
-
import { createHash as
|
|
52389
|
+
import { createHash as createHash5 } from "node:crypto";
|
|
52378
52390
|
import { readdirSync as readdirSync2, readFileSync as readFileSync4, rmdirSync, unlinkSync as unlinkSync5 } from "node:fs";
|
|
52379
52391
|
import { join as join11 } from "node:path";
|
|
52380
52392
|
|
|
@@ -52384,7 +52396,7 @@ init_errors3();
|
|
|
52384
52396
|
import { createHmac as createHmac4 } from "node:crypto";
|
|
52385
52397
|
var BLOB_DEFAULT_TTL_MS = 60 * 60 * 1e3;
|
|
52386
52398
|
var BLOB_MAX_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
52387
|
-
var
|
|
52399
|
+
var MAX_KEY_LENGTH2 = 1024;
|
|
52388
52400
|
var SAFE_SEGMENT = /^[A-Za-z0-9._~-]+$/;
|
|
52389
52401
|
function configError(reason) {
|
|
52390
52402
|
return new McpToolError(`MCP_BLOB_BASE_URL is not usable: ${reason}.`, {
|
|
@@ -52417,8 +52429,8 @@ function badInput(message) {
|
|
|
52417
52429
|
function encodeKey(key) {
|
|
52418
52430
|
if (typeof key !== "string" || key.length === 0)
|
|
52419
52431
|
throw badInput("Blob key must be a non-empty string.");
|
|
52420
|
-
if (key.length >
|
|
52421
|
-
throw badInput(`Blob key is longer than ${
|
|
52432
|
+
if (key.length > MAX_KEY_LENGTH2)
|
|
52433
|
+
throw badInput(`Blob key is longer than ${MAX_KEY_LENGTH2} characters.`);
|
|
52422
52434
|
const segments = key.split("/");
|
|
52423
52435
|
for (const seg of segments) {
|
|
52424
52436
|
if (seg === "" || seg === "." || seg === ".." || !SAFE_SEGMENT.test(seg)) {
|
|
@@ -53075,6 +53087,7 @@ var eventArgsSchema = external_exports.object({
|
|
|
53075
53087
|
attachments: external_exports.boolean().optional()
|
|
53076
53088
|
});
|
|
53077
53089
|
var positionSchema = external_exports.union([
|
|
53090
|
+
external_exports.object({ at: external_exports.object({ t: external_exports.string().min(1), ids: external_exports.array(external_exports.string()) }) }),
|
|
53078
53091
|
external_exports.object({ from: external_exports.string().min(1) }),
|
|
53079
53092
|
external_exports.object({ link: external_exports.string().min(1).optional(), offset: external_exports.number().int().min(0).max(MAX_TOP2) })
|
|
53080
53093
|
]);
|
|
@@ -53285,7 +53298,7 @@ async function linkBatch(pos, limit, first, follow, keep = () => true) {
|
|
|
53285
53298
|
return end;
|
|
53286
53299
|
}
|
|
53287
53300
|
function shortId(id) {
|
|
53288
|
-
return
|
|
53301
|
+
return createHash5("sha256").update(id).digest("hex").slice(0, 16);
|
|
53289
53302
|
}
|
|
53290
53303
|
function uniqueName(name, used) {
|
|
53291
53304
|
let candidate = name;
|
|
@@ -53676,11 +53689,11 @@ function registerExportTools(server, client2, opts = {}) {
|
|
|
53676
53689
|
const folder = args.search !== void 0 ? "all" : args.folder ?? "inbox";
|
|
53677
53690
|
const path = folder === "all" ? "/me/messages" : `/me/mailfolders/${encodeURIComponent(folder)}/messages`;
|
|
53678
53691
|
const perPage = args.attachments === true ? Math.min(pageSize, ATTACHMENT_PAGE_SIZE) : pageSize;
|
|
53679
|
-
const
|
|
53680
|
-
|
|
53692
|
+
const stored = resumed?.pos;
|
|
53693
|
+
const pos = stored === void 0 ? args.search !== void 0 ? { offset: 0 } : { at: { t: since ?? EPOCH, ids: [] } } : "from" in stored ? { at: decodeFrom(stored.from) } : "at" in stored ? { at: decodeFrom(encodeFrom(stored.at)) } : stored;
|
|
53681
53694
|
const fetch2 = async (p, remaining) => {
|
|
53682
|
-
if ("
|
|
53683
|
-
const at =
|
|
53695
|
+
if ("at" in p) {
|
|
53696
|
+
const { at } = p;
|
|
53684
53697
|
const limit = Math.min(remaining, perPage);
|
|
53685
53698
|
const { bound, top } = fromQuery(at, limit);
|
|
53686
53699
|
const filter = until ? `${bound} and ReceivedDateTime lt ${until}` : bound;
|
|
@@ -53697,7 +53710,7 @@ function registerExportTools(server, client2, opts = {}) {
|
|
|
53697
53710
|
posAfter: () => p
|
|
53698
53711
|
};
|
|
53699
53712
|
}
|
|
53700
|
-
if (page.has_more && page.next_from ===
|
|
53713
|
+
if (page.has_more && page.next_from === encodeFrom(at)) {
|
|
53701
53714
|
return {
|
|
53702
53715
|
rows: page.rows,
|
|
53703
53716
|
pos: p,
|
|
@@ -53708,10 +53721,10 @@ function registerExportTools(server, client2, opts = {}) {
|
|
|
53708
53721
|
}
|
|
53709
53722
|
return {
|
|
53710
53723
|
rows: page.rows,
|
|
53711
|
-
pos: {
|
|
53724
|
+
pos: { at: decodeFrom(page.next_from) },
|
|
53712
53725
|
hasMore: page.has_more,
|
|
53713
53726
|
// The first n rows are fresh and ascending, so the keyset position after them is computed the same way.
|
|
53714
|
-
posAfter: (n) => n >= page.rows.length ? {
|
|
53727
|
+
posAfter: (n) => n >= page.rows.length ? { at: decodeFrom(page.next_from) } : { at: decodeFrom(fromPage(page.rows.slice(0, n), at, n, true).next_from) }
|
|
53715
53728
|
};
|
|
53716
53729
|
}
|
|
53717
53730
|
return linkBatch(
|
|
@@ -53799,8 +53812,9 @@ function registerExportTools(server, client2, opts = {}) {
|
|
|
53799
53812
|
}
|
|
53800
53813
|
const prefer = args.time_zone ? `outlook.timezone="${args.time_zone}"` : void 0;
|
|
53801
53814
|
const path = args.calendar !== void 0 ? `/me/calendars/${encodeURIComponent(args.calendar)}/calendarview` : "/me/calendarview";
|
|
53802
|
-
const
|
|
53803
|
-
if ("
|
|
53815
|
+
const stored = resumed?.pos ?? { offset: 0 };
|
|
53816
|
+
if (!("offset" in stored)) throw new McpToolError("`cursor` is not a valid export cursor for this tool.");
|
|
53817
|
+
const pos = stored;
|
|
53804
53818
|
const fetch2 = (p, remaining) => linkBatch(
|
|
53805
53819
|
p,
|
|
53806
53820
|
remaining,
|
package/dist/tools/_paging.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
1
2
|
import { z } from 'zod';
|
|
2
3
|
import { McpToolError } from '@chrischall/mcp-utils';
|
|
3
4
|
import { stripOData } from '../view.js';
|
|
@@ -38,6 +39,19 @@ export function plainCollection(data) {
|
|
|
38
39
|
out.nextLink = next;
|
|
39
40
|
return out;
|
|
40
41
|
}
|
|
42
|
+
/** An Id this short is kept as is (a key, or a test fixture); a real Outlook Id is ~150 characters. */
|
|
43
|
+
const MAX_KEY_LENGTH = 16;
|
|
44
|
+
/**
|
|
45
|
+
* The key a position keeps for a message Id: the Id itself when short, else
|
|
46
|
+
* 12 base64url characters (72 bits) of its SHA-256. Keys only tell apart the
|
|
47
|
+
* messages tied at ONE instant, so a collision is not a practical concern,
|
|
48
|
+
* and a burst of messages in one second costs ~15 characters each in the token
|
|
49
|
+
* rather than a whole Id. A token from an earlier release, carrying whole Ids,
|
|
50
|
+
* is re-keyed on read.
|
|
51
|
+
*/
|
|
52
|
+
export function idKey(id) {
|
|
53
|
+
return id.length <= MAX_KEY_LENGTH ? id : createHash('sha256').update(id).digest('base64url').slice(0, 12);
|
|
54
|
+
}
|
|
41
55
|
export const fromParam = z
|
|
42
56
|
.string()
|
|
43
57
|
.min(1)
|
|
@@ -104,7 +118,7 @@ export function decodeFrom(from) {
|
|
|
104
118
|
!p.ids.every((id) => typeof id === 'string')) {
|
|
105
119
|
throw new McpToolError('`from` is neither an ISO 8601 time nor a next_from token.', { hint: FROM_HINT });
|
|
106
120
|
}
|
|
107
|
-
return { t: utcLiteral(ms), ids: p.ids };
|
|
121
|
+
return { t: utcLiteral(ms), ids: [...new Set(p.ids.map(idKey))] };
|
|
108
122
|
}
|
|
109
123
|
/**
|
|
110
124
|
* The `$filter` date bound and `$top` for a page from `pos`. The bound is
|
|
@@ -121,21 +135,21 @@ export function fromQuery(pos, limit) {
|
|
|
121
135
|
* or the API has another page.
|
|
122
136
|
*/
|
|
123
137
|
export function fromPage(rows, pos, limit, apiHasMore) {
|
|
124
|
-
const seen = new Set(pos.ids);
|
|
125
|
-
const fresh = rows.filter((r) => r.Id === undefined || !seen.has(r.Id));
|
|
138
|
+
const seen = new Set(pos.ids.map(idKey));
|
|
139
|
+
const fresh = rows.filter((r) => r.Id === undefined || !seen.has(idKey(r.Id)));
|
|
126
140
|
const kept = fresh.slice(0, limit);
|
|
127
141
|
const next = nextPosition(kept, pos);
|
|
128
142
|
return { rows: kept, next_from: encodeFrom(next), has_more: fresh.length > limit || apiHasMore };
|
|
129
143
|
}
|
|
130
144
|
function nextPosition(rows, pos) {
|
|
131
145
|
const dated = rows
|
|
132
|
-
.map((r) => ({ id: r.Id, ms: r.ReceivedDateTime ? Date.parse(r.ReceivedDateTime) : NaN }))
|
|
146
|
+
.map((r) => ({ id: r.Id === undefined ? undefined : idKey(r.Id), ms: r.ReceivedDateTime ? Date.parse(r.ReceivedDateTime) : NaN }))
|
|
133
147
|
.filter((r) => r.id !== undefined && !Number.isNaN(r.ms));
|
|
134
148
|
if (dated.length === 0)
|
|
135
149
|
return pos;
|
|
136
150
|
const newest = Math.max(...dated.map((r) => r.ms));
|
|
137
151
|
const tied = dated.filter((r) => r.ms === newest).map((r) => r.id);
|
|
138
152
|
// The position did not move: earlier ties at this instant stay excluded.
|
|
139
|
-
const carried = newest === Date.parse(pos.t) ? pos.ids.filter((id) => !tied.includes(id)) : [];
|
|
153
|
+
const carried = newest === Date.parse(pos.t) ? pos.ids.map(idKey).filter((id) => !tied.includes(id)) : [];
|
|
140
154
|
return { t: utcLiteral(newest), ids: [...carried, ...tied] };
|
|
141
155
|
}
|
package/dist/tools/export.js
CHANGED
|
@@ -158,8 +158,15 @@ const eventArgsSchema = z.object({
|
|
|
158
158
|
format: z.enum(['jsonl', 'markdown']),
|
|
159
159
|
attachments: z.boolean().optional(),
|
|
160
160
|
});
|
|
161
|
-
/**
|
|
161
|
+
/**
|
|
162
|
+
* Where the next part starts: a keyset position, or a nextLink page + rows
|
|
163
|
+
* consumed from it. The keyset position is kept as plain `{ t, ids }` (`at`):
|
|
164
|
+
* the cursor is base64'd as a whole, so a nested `next_from` token would be
|
|
165
|
+
* encoded twice. `from` (that nested token) is still read from cursors an
|
|
166
|
+
* earlier release handed out.
|
|
167
|
+
*/
|
|
162
168
|
const positionSchema = z.union([
|
|
169
|
+
z.object({ at: z.object({ t: z.string().min(1), ids: z.array(z.string()) }) }),
|
|
163
170
|
z.object({ from: z.string().min(1) }),
|
|
164
171
|
z.object({ link: z.string().min(1).optional(), offset: z.number().int().min(0).max(MAX_TOP) }),
|
|
165
172
|
]);
|
|
@@ -877,12 +884,20 @@ export function registerExportTools(server, client, opts = {}) {
|
|
|
877
884
|
const folder = args.search !== undefined ? 'all' : (args.folder ?? 'inbox');
|
|
878
885
|
const path = folder === 'all' ? '/me/messages' : `/me/mailfolders/${encodeURIComponent(folder)}/messages`;
|
|
879
886
|
const perPage = args.attachments === true ? Math.min(pageSize, ATTACHMENT_PAGE_SIZE) : pageSize;
|
|
880
|
-
const
|
|
881
|
-
|
|
882
|
-
|
|
887
|
+
const stored = resumed?.pos;
|
|
888
|
+
// Validate (and normalise) a cursor's keyset position before any work; read an older cursor's nested token.
|
|
889
|
+
const pos = stored === undefined
|
|
890
|
+
? args.search !== undefined
|
|
891
|
+
? { offset: 0 }
|
|
892
|
+
: { at: { t: since ?? EPOCH, ids: [] } }
|
|
893
|
+
: 'from' in stored
|
|
894
|
+
? { at: decodeFrom(stored.from) }
|
|
895
|
+
: 'at' in stored
|
|
896
|
+
? { at: decodeFrom(encodeFrom(stored.at)) }
|
|
897
|
+
: stored;
|
|
883
898
|
const fetch = async (p, remaining) => {
|
|
884
|
-
if ('
|
|
885
|
-
const at =
|
|
899
|
+
if ('at' in p) {
|
|
900
|
+
const { at } = p;
|
|
886
901
|
const limit = Math.min(remaining, perPage);
|
|
887
902
|
const { bound, top } = fromQuery(at, limit);
|
|
888
903
|
const filter = until ? `${bound} and ReceivedDateTime lt ${until}` : bound;
|
|
@@ -900,7 +915,7 @@ export function registerExportTools(server, client, opts = {}) {
|
|
|
900
915
|
posAfter: () => p,
|
|
901
916
|
};
|
|
902
917
|
}
|
|
903
|
-
if (page.has_more && page.next_from ===
|
|
918
|
+
if (page.has_more && page.next_from === encodeFrom(at)) {
|
|
904
919
|
// No row carried a usable ReceivedDateTime, so the position did not
|
|
905
920
|
// move; asking again would return (and duplicate) the same page forever.
|
|
906
921
|
return {
|
|
@@ -913,10 +928,12 @@ export function registerExportTools(server, client, opts = {}) {
|
|
|
913
928
|
}
|
|
914
929
|
return {
|
|
915
930
|
rows: page.rows,
|
|
916
|
-
pos: {
|
|
931
|
+
pos: { at: decodeFrom(page.next_from) },
|
|
917
932
|
hasMore: page.has_more,
|
|
918
933
|
// The first n rows are fresh and ascending, so the keyset position after them is computed the same way.
|
|
919
|
-
posAfter: (n) => n >= page.rows.length
|
|
934
|
+
posAfter: (n) => n >= page.rows.length
|
|
935
|
+
? { at: decodeFrom(page.next_from) }
|
|
936
|
+
: { at: decodeFrom(fromPage(page.rows.slice(0, n), at, n, true).next_from) },
|
|
920
937
|
};
|
|
921
938
|
}
|
|
922
939
|
return linkBatch(p, remaining, () => with429Retry(() => client.get(path, {
|
|
@@ -1010,9 +1027,10 @@ export function registerExportTools(server, client, opts = {}) {
|
|
|
1010
1027
|
}
|
|
1011
1028
|
const prefer = args.time_zone ? `outlook.timezone="${args.time_zone}"` : undefined;
|
|
1012
1029
|
const path = args.calendar !== undefined ? `/me/calendars/${encodeURIComponent(args.calendar)}/calendarview` : '/me/calendarview';
|
|
1013
|
-
const
|
|
1014
|
-
if ('
|
|
1030
|
+
const stored = resumed?.pos ?? { offset: 0 };
|
|
1031
|
+
if (!('offset' in stored))
|
|
1015
1032
|
throw new McpToolError('`cursor` is not a valid export cursor for this tool.');
|
|
1033
|
+
const pos = stored;
|
|
1016
1034
|
const fetch = (p, remaining) => linkBatch(p, remaining, () => with429Retry(() => client.get(path, {
|
|
1017
1035
|
text: true,
|
|
1018
1036
|
...(prefer ? { prefer } : {}),
|
package/dist/version.js
CHANGED
package/mint.yaml
CHANGED
|
@@ -55,6 +55,12 @@ env:
|
|
|
55
55
|
fetchproxy concentrator port used for token capture. Defaults to 37149,
|
|
56
56
|
the fleet-wide shared port. Name this variable in the registration's
|
|
57
57
|
bridgePortEnv for a bridged deployment.
|
|
58
|
+
- name: OUTLOOK_WEB_HOST
|
|
59
|
+
required: false
|
|
60
|
+
help: >-
|
|
61
|
+
Which Outlook Web host the signed-in tab is on: outlook.cloud.microsoft
|
|
62
|
+
(default) or outlook.office.com for a tenant still on the legacy app.
|
|
63
|
+
Only this one host is declared to the browser bridge.
|
|
58
64
|
- name: OUTLOOK_CAPTURE_TIMEOUT
|
|
59
65
|
required: false
|
|
60
66
|
help: >-
|
|
@@ -79,10 +85,10 @@ state:
|
|
|
79
85
|
re-captures from the browser — and on a scale-to-zero machine that means
|
|
80
86
|
re-prompting the user to approve pairing, constantly.
|
|
81
87
|
egress:
|
|
82
|
-
# outlook.office.com is the only host the API client fetches.
|
|
83
|
-
#
|
|
84
|
-
# registration is not blocked on either —
|
|
85
|
-
# current tenant's tab lives.
|
|
88
|
+
# outlook.office.com is the only host the API client fetches. Both Outlook
|
|
89
|
+
# Web hosts a token can be captured from (OUTLOOK_WEB_HOST picks one) are
|
|
90
|
+
# allowed, so a hosted registration is not blocked on either —
|
|
91
|
+
# outlook.cloud.microsoft is where a current tenant's tab lives.
|
|
86
92
|
allow:
|
|
87
93
|
- outlook.cloud.microsoft
|
|
88
94
|
- outlook.office.com
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chrischall/office-outlook-mcp",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.3",
|
|
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>",
|
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.8.
|
|
9
|
+
"version": "0.8.3",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "@chrischall/office-outlook-mcp",
|
|
14
|
-
"version": "0.8.
|
|
14
|
+
"version": "0.8.3",
|
|
15
15
|
"transport": {
|
|
16
16
|
"type": "stdio"
|
|
17
17
|
},
|
|
@@ -41,6 +41,12 @@
|
|
|
41
41
|
"isRequired": false,
|
|
42
42
|
"format": "string"
|
|
43
43
|
},
|
|
44
|
+
{
|
|
45
|
+
"name": "OUTLOOK_WEB_HOST",
|
|
46
|
+
"description": "Which Outlook Web host your signed-in tab is on: outlook.cloud.microsoft (default) or outlook.office.com for a tenant still on the legacy app. Only this one host is declared to the browser bridge.",
|
|
47
|
+
"isRequired": false,
|
|
48
|
+
"format": "string"
|
|
49
|
+
},
|
|
44
50
|
{
|
|
45
51
|
"name": "OUTLOOK_CAPTURE_TIMEOUT",
|
|
46
52
|
"description": "Seconds to wait for the Outlook tab to make a request we can read a token from. Defaults to 30.",
|
|
@@ -13,14 +13,19 @@ token** (~25h lifetime) — every actual request is a plain server-side `curl`.
|
|
|
13
13
|
|
|
14
14
|
```sh
|
|
15
15
|
npm i -g @fetchproxy/cli # provides `fpx`
|
|
16
|
-
fpx profile add outlook --domain outlook.cloud.microsoft
|
|
17
|
-
--domain outlook.office.com
|
|
16
|
+
fpx profile add outlook --domain outlook.cloud.microsoft
|
|
18
17
|
fpx profile declare outlook \
|
|
19
18
|
--capture-header authorization@outlook.cloud.microsoft \
|
|
20
|
-
--capture-header authorization@outlook.office.com \
|
|
21
19
|
--allow-download
|
|
22
20
|
```
|
|
23
21
|
|
|
22
|
+
Declare **one** host, the one your signed-in Outlook tab is on. A tenant still
|
|
23
|
+
on the legacy app uses `outlook.office.com` in both lines and exports
|
|
24
|
+
`OUTLOOK_CAPTURE_HOST=outlook.office.com`. Don't declare both: the bridge opens a
|
|
25
|
+
background tab for every declared domain with no tab open, and because a
|
|
26
|
+
signed-in `outlook.office.com` tab redirects to `outlook.cloud.microsoft`, it
|
|
27
|
+
opens another one on every capture.
|
|
28
|
+
|
|
24
29
|
Declare **all** scopes before the first pairing — widening them later forces the
|
|
25
30
|
user to re-approve. The first `fpx` call prints a 6-digit pair code to approve in
|
|
26
31
|
the ContextMint Bridge extension popup; the grant then persists.
|