@chrischall/office-outlook-mcp 0.8.0 → 0.8.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/README.md +19 -1
- package/dist/auth-fetchproxy.js +65 -57
- package/dist/bundle.js +48 -33
- package/dist/tools/_hints.js +27 -8
- package/dist/tools/mail.js +1 -1
- 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 +12 -5
|
@@ -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.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.8.
|
|
18
|
+
"version": "0.8.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.8.
|
|
4
|
+
"version": "0.8.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/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.2";
|
|
39090
39090
|
}
|
|
39091
39091
|
});
|
|
39092
39092
|
|
|
@@ -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
|
|
|
@@ -50429,7 +50436,7 @@ function workMeta(intent, extra = {}) {
|
|
|
50429
50436
|
return { mint: { domain: "work", intent, ...extra } };
|
|
50430
50437
|
}
|
|
50431
50438
|
var BRIEF_MAX = 120;
|
|
50432
|
-
var BRIEF_NOTE = '"brief": one line per item, `title \u2014 <ISO date or "no date"> \u2014 <status>`, at most 120 chars each, as `data` in the untrusted-content envelope; a last line `-- more` means another page exists (re-ask without "brief" for its nextLink).';
|
|
50439
|
+
var BRIEF_NOTE = '"brief": one line per item, `title \u2014 <ISO date or "no date"> \u2014 <status>`, at most 120 chars each, as `data` in the untrusted-content envelope; a line starting `-- ` is a note (`-- none` for an empty page; a title starting `-- ` or `\\` gets one leading `\\`); a last line `-- more` means another page exists (re-ask without "brief" for its nextLink).';
|
|
50433
50440
|
function briefViewParam(rungs, note) {
|
|
50434
50441
|
const base = viewParam(rungs);
|
|
50435
50442
|
return external_exports.enum([...rungs, "brief"]).optional().describe(`${base.description ?? ""} ${BRIEF_NOTE} ${note}`.trim());
|
|
@@ -50441,15 +50448,23 @@ function flat(s) {
|
|
|
50441
50448
|
function briefDate(dt) {
|
|
50442
50449
|
return flat(dt?.replace(/\.0+(?=Z?$)/, "")) ?? "no date";
|
|
50443
50450
|
}
|
|
50451
|
+
var TITLE_FLOOR = 24;
|
|
50452
|
+
var cut = (s, n) => s.length > n ? `${s.slice(0, Math.max(n - 1, 0))}\u2026` : s;
|
|
50444
50453
|
function briefLine(title, date5, status) {
|
|
50445
|
-
const
|
|
50446
|
-
const shown =
|
|
50447
|
-
const
|
|
50448
|
-
|
|
50454
|
+
const raw = flat(title) ?? "(no subject)";
|
|
50455
|
+
const shown = raw.startsWith("-- ") || raw.startsWith("\\") ? `\\${raw}` : raw;
|
|
50456
|
+
const sep2 = ` \u2014 ${date5} \u2014 `;
|
|
50457
|
+
const room = BRIEF_MAX - sep2.length;
|
|
50458
|
+
if (shown.length + status.length <= room) return `${shown}${sep2}${status}`;
|
|
50459
|
+
const titleRoom = Math.max(room - status.length, Math.min(shown.length, TITLE_FLOOR));
|
|
50460
|
+
const t = cut(shown, titleRoom);
|
|
50461
|
+
return cut(`${t}${sep2}${cut(status, room - t.length)}`, BRIEF_MAX);
|
|
50449
50462
|
}
|
|
50450
50463
|
var BRIEF_MORE = "-- more";
|
|
50464
|
+
var BRIEF_NONE = "-- none";
|
|
50451
50465
|
function briefResult(lines, hasMore = false) {
|
|
50452
|
-
|
|
50466
|
+
const items = lines.length > 0 ? lines : [BRIEF_NONE];
|
|
50467
|
+
return mailboxUntrusted(hasMore ? [...items, BRIEF_MORE] : items);
|
|
50453
50468
|
}
|
|
50454
50469
|
function spaced(value) {
|
|
50455
50470
|
return value.replace(/([a-z])([A-Z])/g, "$1 $2").toLowerCase();
|
|
@@ -50914,7 +50929,7 @@ function registerMailTools(server, client2) {
|
|
|
50914
50929
|
_meta: workMeta("whats-new", { canonical: true, aliases: ["email", "mail", "inbox", "unread", "new mail", "work email"] }),
|
|
50915
50930
|
inputSchema: external_exports.object({
|
|
50916
50931
|
view: external_exports.enum(["brief"]).optional().describe(
|
|
50917
|
-
'Omit for the triage batch above. "brief": one line per message, `<sender>: <subject> \u2014 <received ISO time or "no date"> \u2014 <unread | meeting request | meeting cancelled | meeting response>`, at most 120 chars each, as `data` in the untrusted-content envelope; fetches no body and no invite; a last line `-- more` means another page exists (re-ask without "brief" for its nextLink / next_from).'
|
|
50932
|
+
'Omit for the triage batch above. "brief": one line per message, `<sender>: <subject> \u2014 <received ISO time or "no date"> \u2014 <unread | meeting request | meeting cancelled | meeting response>`, at most 120 chars each, as `data` in the untrusted-content envelope; fetches no body and no invite; a line starting `-- ` is a note (`-- none` for an empty page; a title starting `-- ` or `\\` gets one leading `\\`); a last line `-- more` means another page exists (re-ask without "brief" for its nextLink / next_from).'
|
|
50918
50933
|
),
|
|
50919
50934
|
folder: external_exports.string().min(1).optional().describe('Folder id or well-known name (default "inbox")'),
|
|
50920
50935
|
limit: external_exports.number().int().min(1).max(50).optional().describe("Max messages (default 25, max 50)"),
|
package/dist/tools/_hints.js
CHANGED
|
@@ -20,7 +20,7 @@ export function workMeta(intent, extra = {}) {
|
|
|
20
20
|
}
|
|
21
21
|
/** The longest a brief line may be. */
|
|
22
22
|
export const BRIEF_MAX = 120;
|
|
23
|
-
const BRIEF_NOTE = '"brief": one line per item, `title — <ISO date or "no date"> — <status>`, at most 120 chars each, as `data` in the untrusted-content envelope; a last line `-- more` means another page exists (re-ask without "brief" for its nextLink).';
|
|
23
|
+
const BRIEF_NOTE = '"brief": one line per item, `title — <ISO date or "no date"> — <status>`, at most 120 chars each, as `data` in the untrusted-content envelope; a line starting `-- ` is a note (`-- none` for an empty page; a title starting `-- ` or `\\` gets one leading `\\`); a last line `-- more` means another page exists (re-ask without "brief" for its nextLink).';
|
|
24
24
|
/**
|
|
25
25
|
* The `view` parameter of a canonical read: the tool's own rungs plus
|
|
26
26
|
* `brief`. Absent still means the tool's default.
|
|
@@ -41,26 +41,45 @@ function flat(s) {
|
|
|
41
41
|
export function briefDate(dt) {
|
|
42
42
|
return flat(dt?.replace(/\.0+(?=Z?$)/, '')) ?? 'no date';
|
|
43
43
|
}
|
|
44
|
-
/**
|
|
44
|
+
/** The shortest a title is cut to before the status gives way too. */
|
|
45
|
+
const TITLE_FLOOR = 24;
|
|
46
|
+
const cut = (s, n) => (s.length > n ? `${s.slice(0, Math.max(n - 1, 0))}…` : s);
|
|
47
|
+
/**
|
|
48
|
+
* `title — date — status`, cut to ≤ 120 chars as a WHOLE, title first: the
|
|
49
|
+
* title shortens until the line fits; once it is at its floor the status is
|
|
50
|
+
* cut too (and, past that, the line itself). A title that starts with `-- `
|
|
51
|
+
* or `\` is escaped with one leading `\`, so a subject or sender name can
|
|
52
|
+
* never read as a note — mcp-host strips that one backslash from every item
|
|
53
|
+
* line after its note check.
|
|
54
|
+
*/
|
|
45
55
|
export function briefLine(title, date, status) {
|
|
46
|
-
const
|
|
47
|
-
const shown =
|
|
48
|
-
const
|
|
49
|
-
|
|
56
|
+
const raw = flat(title) ?? '(no subject)';
|
|
57
|
+
const shown = raw.startsWith('-- ') || raw.startsWith('\\') ? `\\${raw}` : raw;
|
|
58
|
+
const sep = ` — ${date} — `;
|
|
59
|
+
const room = BRIEF_MAX - sep.length;
|
|
60
|
+
if (shown.length + status.length <= room)
|
|
61
|
+
return `${shown}${sep}${status}`;
|
|
62
|
+
const titleRoom = Math.max(room - status.length, Math.min(shown.length, TITLE_FLOOR));
|
|
63
|
+
const t = cut(shown, titleRoom);
|
|
64
|
+
return cut(`${t}${sep}${cut(status, room - t.length)}`, BRIEF_MAX);
|
|
50
65
|
}
|
|
51
66
|
/** The note line a brief answer ends with when Outlook holds another page. */
|
|
52
67
|
export const BRIEF_MORE = '-- more';
|
|
68
|
+
/** The single note line a brief answer with no items carries. */
|
|
69
|
+
export const BRIEF_NONE = '-- none';
|
|
53
70
|
/**
|
|
54
71
|
* One line per item, inside the untrusted-data envelope as `data: string[]`
|
|
55
72
|
* (mcp-host's brief-view contract): sender names, subjects and event text are
|
|
56
73
|
* written by other people, so a brief line is fenced like every other rung of
|
|
57
|
-
* these tools. A line starting with `-- ` is a NOTE, not an item: `--
|
|
74
|
+
* these tools. A line starting with `-- ` is a NOTE, not an item: `-- none`
|
|
75
|
+
* answers an empty page, and `-- more`
|
|
58
76
|
* says Outlook has another page (it gives no total, so no `N of M`) — the
|
|
59
77
|
* brief view never silently drops one. Re-ask without `view: "brief"` for the
|
|
60
78
|
* `nextLink` / `next_from` to follow it.
|
|
61
79
|
*/
|
|
62
80
|
export function briefResult(lines, hasMore = false) {
|
|
63
|
-
|
|
81
|
+
const items = lines.length > 0 ? lines : [BRIEF_NONE];
|
|
82
|
+
return mailboxUntrusted(hasMore ? [...items, BRIEF_MORE] : items);
|
|
64
83
|
}
|
|
65
84
|
/** `NotStarted` → `not started`, `TentativelyAccepted` → `tentatively accepted`. */
|
|
66
85
|
export function spaced(value) {
|
package/dist/tools/mail.js
CHANGED
|
@@ -306,7 +306,7 @@ export function registerMailTools(server, client) {
|
|
|
306
306
|
view: z
|
|
307
307
|
.enum(['brief'])
|
|
308
308
|
.optional()
|
|
309
|
-
.describe('Omit for the triage batch above. "brief": one line per message, `<sender>: <subject> — <received ISO time or "no date"> — <unread | meeting request | meeting cancelled | meeting response>`, at most 120 chars each, as `data` in the untrusted-content envelope; fetches no body and no invite; a last line `-- more` means another page exists (re-ask without "brief" for its nextLink / next_from).'),
|
|
309
|
+
.describe('Omit for the triage batch above. "brief": one line per message, `<sender>: <subject> — <received ISO time or "no date"> — <unread | meeting request | meeting cancelled | meeting response>`, at most 120 chars each, as `data` in the untrusted-content envelope; fetches no body and no invite; a line starting `-- ` is a note (`-- none` for an empty page; a title starting `-- ` or `\\` gets one leading `\\`); a last line `-- more` means another page exists (re-ask without "brief" for its nextLink / next_from).'),
|
|
310
310
|
folder: z.string().min(1).optional().describe('Folder id or well-known name (default "inbox")'),
|
|
311
311
|
limit: z.number().int().min(1).max(50).optional().describe('Max messages (default 25, max 50)'),
|
|
312
312
|
sinceHours: z
|
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.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>",
|
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.2",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "@chrischall/office-outlook-mcp",
|
|
14
|
-
"version": "0.8.
|
|
14
|
+
"version": "0.8.2",
|
|
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.
|
|
@@ -86,8 +91,10 @@ When the office-outlook-mcp server is hosted on mcp-host, its tools join the
|
|
|
86
91
|
each answered with `view: "brief"` (one line per item,
|
|
87
92
|
`title — <ISO date | no date> — <status>`, returned inside the
|
|
88
93
|
untrusted-content envelope as `data: string[]` because the text is other
|
|
89
|
-
people's; a
|
|
90
|
-
|
|
94
|
+
people's; a line starting `-- ` is a note — `-- none` answers an empty page,
|
|
95
|
+
and a final `-- more` says Outlook has another page, so re-ask without
|
|
96
|
+
`brief` for the `nextLink`; a title starting `-- ` or `\` is escaped with one
|
|
97
|
+
leading `\`). `brief` is additive: on
|
|
91
98
|
`outlook_list_events` and `outlook_list_tasks` it joins the existing
|
|
92
99
|
`compact | full | raw` values, and `outlook_list_unread_messages` takes a
|
|
93
100
|
new optional `view` whose only value is `brief`:
|