@chrischall/office-outlook-mcp 0.0.0 → 0.1.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 +13 -4
- package/.claude-plugin/mcp.json +11 -0
- package/.claude-plugin/plugin.json +10 -3
- package/.mcp.json +1 -1
- package/README.md +46 -3
- package/dist/auth-fetchproxy.js +68 -10
- package/dist/bundle.js +78 -15
- package/dist/timezone.js +16 -0
- package/dist/tools/calendar.js +10 -4
- package/dist/tools/directory.js +7 -4
- package/dist/tools/mail.js +2 -2
- package/dist/tools/writes.js +4 -2
- package/dist/version.js +1 -1
- package/dist/view.js +21 -0
- package/package.json +2 -2
- package/server.json +5 -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.
|
|
10
|
+
"version": "0.1.2"
|
|
11
11
|
},
|
|
12
12
|
"plugins": [
|
|
13
13
|
{
|
|
@@ -15,12 +15,21 @@
|
|
|
15
15
|
"displayName": "Outlook (Microsoft 365)",
|
|
16
16
|
"source": "./",
|
|
17
17
|
"description": "MCP server for Outlook / Microsoft 365 — read mail, folders, calendar, contacts and tasks, and send mail with confirmation",
|
|
18
|
-
"version": "0.
|
|
19
|
-
"author": {
|
|
18
|
+
"version": "0.1.2",
|
|
19
|
+
"author": {
|
|
20
|
+
"name": "Chris Hall"
|
|
21
|
+
},
|
|
20
22
|
"homepage": "https://github.com/chrischall/office-outlook-mcp",
|
|
21
23
|
"repository": "https://github.com/chrischall/office-outlook-mcp",
|
|
22
24
|
"license": "MIT",
|
|
23
|
-
"keywords": [
|
|
25
|
+
"keywords": [
|
|
26
|
+
"outlook",
|
|
27
|
+
"office365",
|
|
28
|
+
"microsoft365",
|
|
29
|
+
"email",
|
|
30
|
+
"calendar",
|
|
31
|
+
"mcp"
|
|
32
|
+
],
|
|
24
33
|
"category": "productivity"
|
|
25
34
|
}
|
|
26
35
|
]
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "office-outlook-mcp",
|
|
3
3
|
"displayName": "Outlook (Microsoft 365)",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.1.2",
|
|
5
5
|
"description": "Outlook / Microsoft 365 for Claude — read mail, folders, calendar, contacts and tasks, and send mail with confirmation, using your signed-in browser session",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Chris Hall",
|
|
@@ -10,7 +10,14 @@
|
|
|
10
10
|
"homepage": "https://github.com/chrischall/office-outlook-mcp",
|
|
11
11
|
"repository": "https://github.com/chrischall/office-outlook-mcp",
|
|
12
12
|
"license": "MIT",
|
|
13
|
-
"keywords": [
|
|
13
|
+
"keywords": [
|
|
14
|
+
"outlook",
|
|
15
|
+
"office365",
|
|
16
|
+
"microsoft365",
|
|
17
|
+
"email",
|
|
18
|
+
"calendar",
|
|
19
|
+
"mcp"
|
|
20
|
+
],
|
|
14
21
|
"skills": "./skills/",
|
|
15
|
-
"mcp": "
|
|
22
|
+
"mcp": "./mcp.json"
|
|
16
23
|
}
|
package/.mcp.json
CHANGED
package/README.md
CHANGED
|
@@ -37,6 +37,42 @@ npm i -g @fetchproxy/cli
|
|
|
37
37
|
The first capture prints a 6-digit pair code to approve in the Transporter
|
|
38
38
|
popup; the grant persists.
|
|
39
39
|
|
|
40
|
+
### Install in opencode
|
|
41
|
+
|
|
42
|
+
opencode reads MCP servers from `opencode.json` (project) or
|
|
43
|
+
`~/.config/opencode/opencode.json` (global):
|
|
44
|
+
|
|
45
|
+
```json
|
|
46
|
+
{
|
|
47
|
+
"mcp": {
|
|
48
|
+
"servers": {
|
|
49
|
+
"outlook": {
|
|
50
|
+
"type": "local",
|
|
51
|
+
"command": ["npx", "-y", "@chrischall/office-outlook-mcp"]
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
That one file serves **opencode 2 and current opencode 1** — verified live on
|
|
59
|
+
2.0.11, 1.18.31 and 1.18.28, which all connect from it.
|
|
60
|
+
|
|
61
|
+
Two things worth knowing, both measured rather than assumed:
|
|
62
|
+
|
|
63
|
+
- **Never carry both config shapes in one file.** Older opencode 1 wants the
|
|
64
|
+
servers directly under `mcp` (`"mcp": { "outlook": { … } }`), and the
|
|
65
|
+
temptation is to write both so either version finds one. Do not: given a
|
|
66
|
+
sibling key beside `servers`, opencode 2 parses the file — it still shows up
|
|
67
|
+
in `opencode debug config` — and then reports *"No MCP servers configured"*.
|
|
68
|
+
No error, no warning, every server silently gone.
|
|
69
|
+
- **An opencode 1 old enough to reject `servers` says so loudly**
|
|
70
|
+
(`Configuration is invalid at …`), so if you see that, switch that machine to
|
|
71
|
+
the flat shape rather than combining them.
|
|
72
|
+
|
|
73
|
+
opencode 2 talks to a background service, so `opencode reload` before
|
|
74
|
+
`opencode mcp list` after editing config.
|
|
75
|
+
|
|
40
76
|
### Configuration
|
|
41
77
|
|
|
42
78
|
Everything is optional — with nothing set, the server captures from the browser.
|
|
@@ -47,7 +83,7 @@ Everything is optional — with nothing set, the server captures from the browse
|
|
|
47
83
|
| `OUTLOOK_DISABLE_FETCHPROXY` | `1` to disable browser capture, requiring `OUTLOOK_ACCESS_TOKEN`. |
|
|
48
84
|
| `OUTLOOK_API_BASE` | Override the REST base URL. Defaults to `https://outlook.office.com/api/v2.0`. |
|
|
49
85
|
| `OUTLOOK_WS_PORT` | fetchproxy concentrator port. Defaults to `37149`, the fleet-wide shared port. |
|
|
50
|
-
| `OUTLOOK_CAPTURE_TIMEOUT` | Seconds to wait for the tab to make a readable request. Defaults to `30`. |
|
|
86
|
+
| `OUTLOOK_CAPTURE_TIMEOUT` | Seconds to wait for the tab to make a readable request. Bounds the WHOLE capture, both declared hosts included. Defaults to `30`. |
|
|
51
87
|
| `OUTLOOK_TOKEN_CACHE` | `false` to stop caching the token between runs. |
|
|
52
88
|
| `OUTLOOK_TOKEN_FILE` | Path to the cached token. Defaults to `~/.office-outlook-mcp/token.json`. |
|
|
53
89
|
|
|
@@ -66,8 +102,10 @@ Everything is optional — with nothing set, the server captures from the browse
|
|
|
66
102
|
**Diagnostics** — `outlook_healthcheck`
|
|
67
103
|
|
|
68
104
|
Every read tool takes `view: compact | full | raw`, defaulting to **compact**.
|
|
69
|
-
Mutating tools
|
|
70
|
-
dry-run preview of exactly what would be sent.
|
|
105
|
+
Mutating tools **write nothing** without `confirm: true` — they return a
|
|
106
|
+
dry-run preview of exactly what would be sent. (`outlook_create_event` first
|
|
107
|
+
reads the mailbox time zone, so its preview can name the zone it would book
|
|
108
|
+
in; that is the one read a dry run makes.)
|
|
71
109
|
|
|
72
110
|
## Things worth knowing
|
|
73
111
|
|
|
@@ -81,6 +119,11 @@ dry-run preview of exactly what would be sent.
|
|
|
81
119
|
- `search` and `unreadOnly` cannot be combined; the API rejects `$search`
|
|
82
120
|
alongside `$filter`, so the tool refuses before making a doomed request.
|
|
83
121
|
- Moving a message assigns it a **new id**.
|
|
122
|
+
- `outlook_list_events` returns times in the **mailbox's own time zone** unless
|
|
123
|
+
`timeZone` overrides it. The API itself defaults to UTC, which silently reads
|
|
124
|
+
as a four-hour error on an Eastern mailbox.
|
|
125
|
+
- Contact phone fields are `MobilePhone1`, not Graph's `MobilePhone`. The v2.0
|
|
126
|
+
Contact type rejects the Graph name with a 400.
|
|
84
127
|
|
|
85
128
|
## The lightweight alternative
|
|
86
129
|
|
package/dist/auth-fetchproxy.js
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* - that token authenticates plain server-side requests to
|
|
17
17
|
* `outlook.office.com/api/v2.0` with no bridge involved;
|
|
18
18
|
* - older tenants still serve the app from `outlook.office.com`, so both
|
|
19
|
-
* hosts are declared and captured
|
|
19
|
+
* hosts are declared and captured, under ONE overall window.
|
|
20
20
|
*
|
|
21
21
|
* We do NOT try to mint a token by calling an endpoint ourselves. There is no
|
|
22
22
|
* such endpoint reachable from the page's origin — the token comes from MSAL's
|
|
@@ -56,6 +56,27 @@ const CAPTURE_DECL_OFFICE = {
|
|
|
56
56
|
path: '/*',
|
|
57
57
|
headerName: 'authorization',
|
|
58
58
|
};
|
|
59
|
+
/**
|
|
60
|
+
* The hosts a token may be captured from — the two Outlook Web origins.
|
|
61
|
+
*
|
|
62
|
+
* Exported so the trust boundary below can be derived from them rather than
|
|
63
|
+
* restated.
|
|
64
|
+
*/
|
|
65
|
+
export const CAPTURE_HOSTS = [CAPTURE_DECL_CLOUD.host, CAPTURE_DECL_OFFICE.host];
|
|
66
|
+
/**
|
|
67
|
+
* The extension's trust boundary: exactly the hosts we capture from.
|
|
68
|
+
*
|
|
69
|
+
* These were the apex domains `cloud.microsoft` and `office.com`, which the
|
|
70
|
+
* extension reads as "any subdomain of either" — the whole of Microsoft 365
|
|
71
|
+
* granted to read one header off two known hosts. It is also what sent the
|
|
72
|
+
* pairing flow to `m365.cloud.microsoft/chat`, a tab nothing here needs.
|
|
73
|
+
*
|
|
74
|
+
* Narrowing costs nothing: the capture declarations were always these two
|
|
75
|
+
* hosts, and every API call the server makes afterwards is a plain server-side
|
|
76
|
+
* `fetch` that the bridge never sees. Verified live 2026-09-20 — capture still
|
|
77
|
+
* succeeded, and an already-paired extension did not ask to re-pair.
|
|
78
|
+
*/
|
|
79
|
+
export const TRUST_DOMAINS = [...CAPTURE_HOSTS];
|
|
59
80
|
/**
|
|
60
81
|
* How long to wait for the page to make a request we can read.
|
|
61
82
|
*
|
|
@@ -97,6 +118,45 @@ function withFetch(opts) {
|
|
|
97
118
|
: [...derived, 'fetch'],
|
|
98
119
|
};
|
|
99
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
|
+
}
|
|
100
160
|
/** Normalise a captured header to a bare token, dropping any `Bearer ` prefix. */
|
|
101
161
|
export function stripBearer(raw) {
|
|
102
162
|
const m = /^\s*Bearer\s+(.+)$/i.exec(raw);
|
|
@@ -105,13 +165,15 @@ export function stripBearer(raw) {
|
|
|
105
165
|
/**
|
|
106
166
|
* Capture an Outlook access token from the user's signed-in browser tab.
|
|
107
167
|
*
|
|
108
|
-
* Both hosts are
|
|
109
|
-
* other simply never resolves.
|
|
168
|
+
* Both hosts are attempted: whichever the tab is actually on answers first, and
|
|
169
|
+
* the other simply never resolves. The bridge serializes them, so the pair is
|
|
170
|
+
* held to one deadline by `raceCaptures` rather than to one deadline each.
|
|
171
|
+
* Returns the bare token (no `Bearer ` prefix).
|
|
110
172
|
*/
|
|
111
173
|
export async function captureTokenViaFetchproxy() {
|
|
112
174
|
const transport = createFetchproxyTransport({
|
|
113
175
|
...withFetch(createBootstrapOpts({
|
|
114
|
-
domains: [
|
|
176
|
+
domains: [...TRUST_DOMAINS],
|
|
115
177
|
bootstrap: {
|
|
116
178
|
captureHeaders: [{ ...CAPTURE_DECL_CLOUD }, { ...CAPTURE_DECL_OFFICE }],
|
|
117
179
|
},
|
|
@@ -140,15 +202,11 @@ export async function captureTokenViaFetchproxy() {
|
|
|
140
202
|
}
|
|
141
203
|
return token;
|
|
142
204
|
}));
|
|
143
|
-
// `any` resolves on the first SUCCESS rather than the first settle, so the
|
|
144
|
-
// host the user is not on failing does not sink the one they are on.
|
|
145
205
|
try {
|
|
146
|
-
return await
|
|
206
|
+
return await raceCaptures(attempts, CAPTURE_TIMEOUT_MS);
|
|
147
207
|
}
|
|
148
208
|
catch (e) {
|
|
149
|
-
const detail = e
|
|
150
|
-
? e.errors.map((x) => x.message).join('; ')
|
|
151
|
-
: e.message;
|
|
209
|
+
const detail = e.message;
|
|
152
210
|
throw new Error(`could not capture an Outlook token from the browser (${detail}). ` +
|
|
153
211
|
'Open a signed-in Outlook tab (outlook.cloud.microsoft or ' +
|
|
154
212
|
'outlook.office.com), leave it in the foreground for a moment so the ' +
|
package/dist/bundle.js
CHANGED
|
@@ -35843,7 +35843,7 @@ var init_version = __esm({
|
|
|
35843
35843
|
"src/version.ts"() {
|
|
35844
35844
|
"use strict";
|
|
35845
35845
|
PACKAGE_NAME = "office-outlook-mcp";
|
|
35846
|
-
VERSION = "0.
|
|
35846
|
+
VERSION = "0.1.2";
|
|
35847
35847
|
}
|
|
35848
35848
|
});
|
|
35849
35849
|
|
|
@@ -44889,8 +44889,11 @@ var init_fetchproxy = __esm({
|
|
|
44889
44889
|
// src/auth-fetchproxy.ts
|
|
44890
44890
|
var auth_fetchproxy_exports = {};
|
|
44891
44891
|
__export(auth_fetchproxy_exports, {
|
|
44892
|
+
CAPTURE_HOSTS: () => CAPTURE_HOSTS,
|
|
44893
|
+
TRUST_DOMAINS: () => TRUST_DOMAINS,
|
|
44892
44894
|
captureTokenViaFetchproxy: () => captureTokenViaFetchproxy,
|
|
44893
44895
|
getWsPort: () => getWsPort,
|
|
44896
|
+
raceCaptures: () => raceCaptures,
|
|
44894
44897
|
stripBearer: () => stripBearer
|
|
44895
44898
|
});
|
|
44896
44899
|
function getWsPort() {
|
|
@@ -44903,6 +44906,33 @@ function withFetch(opts) {
|
|
|
44903
44906
|
capabilities: derived2.includes("fetch") ? derived2 : [...derived2, "fetch"]
|
|
44904
44907
|
};
|
|
44905
44908
|
}
|
|
44909
|
+
async function raceCaptures(attempts, windowMs) {
|
|
44910
|
+
for (const a of attempts) a.catch(() => {
|
|
44911
|
+
});
|
|
44912
|
+
let timer;
|
|
44913
|
+
const expiry = new Promise((_, reject) => {
|
|
44914
|
+
timer = setTimeout(
|
|
44915
|
+
() => reject(
|
|
44916
|
+
new Error(
|
|
44917
|
+
`timed out after ${Math.round(windowMs / 1e3)}s waiting for the page to make a request we could read`
|
|
44918
|
+
)
|
|
44919
|
+
),
|
|
44920
|
+
windowMs
|
|
44921
|
+
);
|
|
44922
|
+
});
|
|
44923
|
+
try {
|
|
44924
|
+
return await Promise.race([
|
|
44925
|
+
Promise.any(attempts).catch((e) => {
|
|
44926
|
+
throw new Error(
|
|
44927
|
+
e instanceof AggregateError ? e.errors.map((x) => x.message).join("; ") : e.message
|
|
44928
|
+
);
|
|
44929
|
+
}),
|
|
44930
|
+
expiry
|
|
44931
|
+
]);
|
|
44932
|
+
} finally {
|
|
44933
|
+
clearTimeout(timer);
|
|
44934
|
+
}
|
|
44935
|
+
}
|
|
44906
44936
|
function stripBearer(raw) {
|
|
44907
44937
|
const m = /^\s*Bearer\s+(.+)$/i.exec(raw);
|
|
44908
44938
|
return (m ? m[1] : raw).trim();
|
|
@@ -44911,7 +44941,7 @@ async function captureTokenViaFetchproxy() {
|
|
|
44911
44941
|
const transport = createFetchproxyTransport({
|
|
44912
44942
|
...withFetch(
|
|
44913
44943
|
createBootstrapOpts({
|
|
44914
|
-
domains: [
|
|
44944
|
+
domains: [...TRUST_DOMAINS],
|
|
44915
44945
|
bootstrap: {
|
|
44916
44946
|
captureHeaders: [{ ...CAPTURE_DECL_CLOUD }, { ...CAPTURE_DECL_OFFICE }]
|
|
44917
44947
|
}
|
|
@@ -44939,9 +44969,9 @@ async function captureTokenViaFetchproxy() {
|
|
|
44939
44969
|
})
|
|
44940
44970
|
);
|
|
44941
44971
|
try {
|
|
44942
|
-
return await
|
|
44972
|
+
return await raceCaptures(attempts, CAPTURE_TIMEOUT_MS);
|
|
44943
44973
|
} catch (e) {
|
|
44944
|
-
const detail = e
|
|
44974
|
+
const detail = e.message;
|
|
44945
44975
|
throw new Error(
|
|
44946
44976
|
`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.`
|
|
44947
44977
|
);
|
|
@@ -44953,7 +44983,7 @@ async function captureTokenViaFetchproxy() {
|
|
|
44953
44983
|
}
|
|
44954
44984
|
}
|
|
44955
44985
|
}
|
|
44956
|
-
var DEFAULT_WS_PORT, CAPTURE_DECL_CLOUD, CAPTURE_DECL_OFFICE, CAPTURE_TIMEOUT_MS, BRIDGE_DEADLINE_MS, MIN_TOKEN_LENGTH;
|
|
44986
|
+
var DEFAULT_WS_PORT, CAPTURE_DECL_CLOUD, CAPTURE_DECL_OFFICE, CAPTURE_HOSTS, TRUST_DOMAINS, CAPTURE_TIMEOUT_MS, BRIDGE_DEADLINE_MS, MIN_TOKEN_LENGTH;
|
|
44957
44987
|
var init_auth_fetchproxy = __esm({
|
|
44958
44988
|
"src/auth-fetchproxy.ts"() {
|
|
44959
44989
|
"use strict";
|
|
@@ -44971,6 +45001,8 @@ var init_auth_fetchproxy = __esm({
|
|
|
44971
45001
|
path: "/*",
|
|
44972
45002
|
headerName: "authorization"
|
|
44973
45003
|
};
|
|
45004
|
+
CAPTURE_HOSTS = [CAPTURE_DECL_CLOUD.host, CAPTURE_DECL_OFFICE.host];
|
|
45005
|
+
TRUST_DOMAINS = [...CAPTURE_HOSTS];
|
|
44974
45006
|
CAPTURE_TIMEOUT_MS = readTtlMsEnv("OUTLOOK_CAPTURE_TIMEOUT", 3e4);
|
|
44975
45007
|
BRIDGE_DEADLINE_MS = Math.max(CAPTURE_TIMEOUT_MS + 15e3, 3e4);
|
|
44976
45008
|
MIN_TOKEN_LENGTH = 100;
|
|
@@ -45625,6 +45657,13 @@ function compactFolder(f) {
|
|
|
45625
45657
|
Total: f.TotalItemCount
|
|
45626
45658
|
});
|
|
45627
45659
|
}
|
|
45660
|
+
function stripOData(o) {
|
|
45661
|
+
const out = {};
|
|
45662
|
+
for (const [k, v] of Object.entries(o)) {
|
|
45663
|
+
if (!k.startsWith("@odata.")) out[k] = v;
|
|
45664
|
+
}
|
|
45665
|
+
return out;
|
|
45666
|
+
}
|
|
45628
45667
|
function pruned(o) {
|
|
45629
45668
|
const out = {};
|
|
45630
45669
|
for (const [k, v] of Object.entries(o)) {
|
|
@@ -45763,7 +45802,7 @@ function registerMailTools(server, client2) {
|
|
|
45763
45802
|
);
|
|
45764
45803
|
return minifiedResult({
|
|
45765
45804
|
count: data.value?.length ?? 0,
|
|
45766
|
-
items: data.value ?? []
|
|
45805
|
+
items: (data.value ?? []).map(stripOData)
|
|
45767
45806
|
});
|
|
45768
45807
|
}
|
|
45769
45808
|
);
|
|
@@ -45772,6 +45811,23 @@ function registerMailTools(server, client2) {
|
|
|
45772
45811
|
// src/tools/calendar.ts
|
|
45773
45812
|
init_zod();
|
|
45774
45813
|
init_dist2();
|
|
45814
|
+
|
|
45815
|
+
// src/timezone.ts
|
|
45816
|
+
var zoneByClient = /* @__PURE__ */ new WeakMap();
|
|
45817
|
+
async function mailboxTimeZone(client2) {
|
|
45818
|
+
const cached2 = zoneByClient.get(client2);
|
|
45819
|
+
if (cached2) return cached2;
|
|
45820
|
+
try {
|
|
45821
|
+
const settings = await client2.get("/me/MailboxSettings");
|
|
45822
|
+
const zone = settings?.TimeZone?.trim() || void 0;
|
|
45823
|
+
if (zone) zoneByClient.set(client2, zone);
|
|
45824
|
+
return zone;
|
|
45825
|
+
} catch {
|
|
45826
|
+
return void 0;
|
|
45827
|
+
}
|
|
45828
|
+
}
|
|
45829
|
+
|
|
45830
|
+
// src/tools/calendar.ts
|
|
45775
45831
|
var EVENT_SELECT = "Id,Subject,Start,End,Location,Organizer,IsAllDay,IsCancelled,ShowAs,OnlineMeetingUrl,BodyPreview";
|
|
45776
45832
|
function qs2(params) {
|
|
45777
45833
|
const parts = [];
|
|
@@ -45798,6 +45854,7 @@ function registerCalendarTools(server, client2) {
|
|
|
45798
45854
|
})
|
|
45799
45855
|
},
|
|
45800
45856
|
async ({ view, start, end, timeZone, limit }) => {
|
|
45857
|
+
const zone = timeZone ?? await mailboxTimeZone(client2);
|
|
45801
45858
|
const data = await client2.get(
|
|
45802
45859
|
`/me/calendarview${qs2({
|
|
45803
45860
|
startDateTime: start,
|
|
@@ -45806,7 +45863,7 @@ function registerCalendarTools(server, client2) {
|
|
|
45806
45863
|
$orderby: "Start/DateTime",
|
|
45807
45864
|
$top: limit ?? 50
|
|
45808
45865
|
})}`,
|
|
45809
|
-
|
|
45866
|
+
zone ? { prefer: `outlook.timezone="${zone}"` } : {}
|
|
45810
45867
|
);
|
|
45811
45868
|
const v = resolveView(view, VIEWS2);
|
|
45812
45869
|
if (v === "raw") return minifiedResult(data);
|
|
@@ -45827,9 +45884,10 @@ function registerCalendarTools(server, client2) {
|
|
|
45827
45884
|
})
|
|
45828
45885
|
},
|
|
45829
45886
|
async ({ view, id, timeZone }) => {
|
|
45887
|
+
const zone = timeZone ?? await mailboxTimeZone(client2);
|
|
45830
45888
|
const data = await client2.get(
|
|
45831
45889
|
`/me/events/${encodeURIComponent(id)}`,
|
|
45832
|
-
|
|
45890
|
+
zone ? { prefer: `outlook.timezone="${zone}"` } : {}
|
|
45833
45891
|
);
|
|
45834
45892
|
const v = resolveView(view, VIEWS2);
|
|
45835
45893
|
if (v === "raw") return minifiedResult(data);
|
|
@@ -45850,7 +45908,10 @@ function registerCalendarTools(server, client2) {
|
|
|
45850
45908
|
`/me/calendars${qs2({ $select: "Id,Name,Color,CanEdit,Owner" })}`
|
|
45851
45909
|
);
|
|
45852
45910
|
if (resolveView(view, VIEWS2) === "raw") return minifiedResult(data);
|
|
45853
|
-
return minifiedResult({
|
|
45911
|
+
return minifiedResult({
|
|
45912
|
+
count: data.value?.length ?? 0,
|
|
45913
|
+
items: (data.value ?? []).map(stripOData)
|
|
45914
|
+
});
|
|
45854
45915
|
}
|
|
45855
45916
|
);
|
|
45856
45917
|
}
|
|
@@ -45868,7 +45929,7 @@ function qs3(params) {
|
|
|
45868
45929
|
}
|
|
45869
45930
|
function collection(data, raw) {
|
|
45870
45931
|
if (raw) return data;
|
|
45871
|
-
return { count: data.value?.length ?? 0, items: data.value ?? [] };
|
|
45932
|
+
return { count: data.value?.length ?? 0, items: (data.value ?? []).map(stripOData) };
|
|
45872
45933
|
}
|
|
45873
45934
|
function registerDirectoryTools(server, client2) {
|
|
45874
45935
|
server.registerTool(
|
|
@@ -45878,7 +45939,7 @@ function registerDirectoryTools(server, client2) {
|
|
|
45878
45939
|
annotations: { readOnlyHint: true },
|
|
45879
45940
|
inputSchema: external_exports.object({})
|
|
45880
45941
|
},
|
|
45881
|
-
async () => minifiedResult(await client2.get("/me"))
|
|
45942
|
+
async () => minifiedResult(stripOData(await client2.get("/me")))
|
|
45882
45943
|
);
|
|
45883
45944
|
server.registerTool(
|
|
45884
45945
|
"outlook_get_mailbox_settings",
|
|
@@ -45887,7 +45948,7 @@ function registerDirectoryTools(server, client2) {
|
|
|
45887
45948
|
annotations: { readOnlyHint: true },
|
|
45888
45949
|
inputSchema: external_exports.object({})
|
|
45889
45950
|
},
|
|
45890
|
-
async () => minifiedResult(await client2.get("/me/MailboxSettings"))
|
|
45951
|
+
async () => minifiedResult(stripOData(await client2.get("/me/MailboxSettings")))
|
|
45891
45952
|
);
|
|
45892
45953
|
server.registerTool(
|
|
45893
45954
|
"outlook_list_contacts",
|
|
@@ -45905,7 +45966,9 @@ function registerDirectoryTools(server, client2) {
|
|
|
45905
45966
|
`/me/contacts${qs3({
|
|
45906
45967
|
$top: limit ?? 50,
|
|
45907
45968
|
$skip: skip,
|
|
45908
|
-
|
|
45969
|
+
// `MobilePhone1`, not `MobilePhone`: the latter is the Graph name and
|
|
45970
|
+
// the v2.0 Contact type rejects it outright with a 400.
|
|
45971
|
+
$select: "Id,DisplayName,EmailAddresses,CompanyName,JobTitle,MobilePhone1"
|
|
45909
45972
|
})}`
|
|
45910
45973
|
);
|
|
45911
45974
|
return minifiedResult(collection(data, resolveView(view, VIEWS2) === "raw"));
|
|
@@ -46133,7 +46196,7 @@ function registerWriteTools(server, client2) {
|
|
|
46133
46196
|
subject: external_exports.string().describe("Event title"),
|
|
46134
46197
|
start: external_exports.string().min(1).describe("Start, ISO 8601 local time e.g. 2026-09-22T15:00:00"),
|
|
46135
46198
|
end: external_exports.string().min(1).describe("End, ISO 8601 local time"),
|
|
46136
|
-
timeZone: external_exports.string().optional().describe(
|
|
46199
|
+
timeZone: external_exports.string().optional().describe("Windows time-zone name. Defaults to the MAILBOX time zone."),
|
|
46137
46200
|
location: external_exports.string().optional().describe("Location display name"),
|
|
46138
46201
|
body: external_exports.string().optional().describe("Event description"),
|
|
46139
46202
|
attendees: recipientList,
|
|
@@ -46141,7 +46204,7 @@ function registerWriteTools(server, client2) {
|
|
|
46141
46204
|
})
|
|
46142
46205
|
},
|
|
46143
46206
|
async ({ subject, start, end, timeZone, location, body, attendees, confirm }) => {
|
|
46144
|
-
const tz = timeZone ?? "UTC";
|
|
46207
|
+
const tz = timeZone ?? await mailboxTimeZone(client2) ?? "UTC";
|
|
46145
46208
|
const payload = {
|
|
46146
46209
|
Subject: subject,
|
|
46147
46210
|
Start: { DateTime: start, TimeZone: tz },
|
package/dist/timezone.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
const zoneByClient = new WeakMap();
|
|
2
|
+
export async function mailboxTimeZone(client) {
|
|
3
|
+
const cached = zoneByClient.get(client);
|
|
4
|
+
if (cached)
|
|
5
|
+
return cached;
|
|
6
|
+
try {
|
|
7
|
+
const settings = await client.get('/me/MailboxSettings');
|
|
8
|
+
const zone = settings?.TimeZone?.trim() || undefined;
|
|
9
|
+
if (zone)
|
|
10
|
+
zoneByClient.set(client, zone);
|
|
11
|
+
return zone;
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
}
|
package/dist/tools/calendar.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { minifiedResult, resolveView, viewParam } from '@chrischall/mcp-utils';
|
|
3
|
-
import { compactEvent, fullEvent, projectCollection } from '../view.js';
|
|
3
|
+
import { compactEvent, fullEvent, projectCollection, stripOData, } from '../view.js';
|
|
4
4
|
import { VIEWS } from './mail.js';
|
|
5
|
+
import { mailboxTimeZone } from '../timezone.js';
|
|
5
6
|
const EVENT_SELECT = 'Id,Subject,Start,End,Location,Organizer,IsAllDay,IsCancelled,ShowAs,OnlineMeetingUrl,BodyPreview';
|
|
6
7
|
function qs(params) {
|
|
7
8
|
const parts = [];
|
|
@@ -30,13 +31,14 @@ export function registerCalendarTools(server, client) {
|
|
|
30
31
|
limit: z.number().int().min(1).max(200).optional().describe('Max events (default 50)'),
|
|
31
32
|
}),
|
|
32
33
|
}, async ({ view, start, end, timeZone, limit }) => {
|
|
34
|
+
const zone = timeZone ?? (await mailboxTimeZone(client));
|
|
33
35
|
const data = await client.get(`/me/calendarview${qs({
|
|
34
36
|
startDateTime: start,
|
|
35
37
|
endDateTime: end,
|
|
36
38
|
$select: EVENT_SELECT,
|
|
37
39
|
$orderby: 'Start/DateTime',
|
|
38
40
|
$top: limit ?? 50,
|
|
39
|
-
})}`,
|
|
41
|
+
})}`, zone ? { prefer: `outlook.timezone="${zone}"` } : {});
|
|
40
42
|
const v = resolveView(view, VIEWS);
|
|
41
43
|
if (v === 'raw')
|
|
42
44
|
return minifiedResult(data);
|
|
@@ -51,7 +53,8 @@ export function registerCalendarTools(server, client) {
|
|
|
51
53
|
timeZone: z.string().optional().describe('Windows time-zone name for returned times'),
|
|
52
54
|
}),
|
|
53
55
|
}, async ({ view, id, timeZone }) => {
|
|
54
|
-
const
|
|
56
|
+
const zone = timeZone ?? (await mailboxTimeZone(client));
|
|
57
|
+
const data = await client.get(`/me/events/${encodeURIComponent(id)}`, zone ? { prefer: `outlook.timezone="${zone}"` } : {});
|
|
55
58
|
const v = resolveView(view, VIEWS);
|
|
56
59
|
if (v === 'raw')
|
|
57
60
|
return minifiedResult(data);
|
|
@@ -67,6 +70,9 @@ export function registerCalendarTools(server, client) {
|
|
|
67
70
|
const data = await client.get(`/me/calendars${qs({ $select: 'Id,Name,Color,CanEdit,Owner' })}`);
|
|
68
71
|
if (resolveView(view, VIEWS) === 'raw')
|
|
69
72
|
return minifiedResult(data);
|
|
70
|
-
return minifiedResult({
|
|
73
|
+
return minifiedResult({
|
|
74
|
+
count: data.value?.length ?? 0,
|
|
75
|
+
items: (data.value ?? []).map(stripOData),
|
|
76
|
+
});
|
|
71
77
|
});
|
|
72
78
|
}
|
package/dist/tools/directory.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { minifiedResult, resolveView, viewParam } from '@chrischall/mcp-utils';
|
|
3
|
+
import { stripOData } from '../view.js';
|
|
3
4
|
import { VIEWS } from './mail.js';
|
|
4
5
|
function qs(params) {
|
|
5
6
|
const parts = [];
|
|
@@ -13,19 +14,19 @@ function qs(params) {
|
|
|
13
14
|
function collection(data, raw) {
|
|
14
15
|
if (raw)
|
|
15
16
|
return data;
|
|
16
|
-
return { count: data.value?.length ?? 0, items: data.value ?? [] };
|
|
17
|
+
return { count: data.value?.length ?? 0, items: (data.value ?? []).map(stripOData) };
|
|
17
18
|
}
|
|
18
19
|
export function registerDirectoryTools(server, client) {
|
|
19
20
|
server.registerTool('outlook_get_profile', {
|
|
20
21
|
description: "Get the signed-in mailbox's identity (email address, display name, alias). The cheapest call that proves the credential works.",
|
|
21
22
|
annotations: { readOnlyHint: true },
|
|
22
23
|
inputSchema: z.object({}),
|
|
23
|
-
}, async () => minifiedResult(await client.get('/me')));
|
|
24
|
+
}, async () => minifiedResult(stripOData(await client.get('/me'))));
|
|
24
25
|
server.registerTool('outlook_get_mailbox_settings', {
|
|
25
26
|
description: 'Get mailbox settings: time zone, working hours, language, and automatic-replies (out-of-office) configuration.',
|
|
26
27
|
annotations: { readOnlyHint: true },
|
|
27
28
|
inputSchema: z.object({}),
|
|
28
|
-
}, async () => minifiedResult(await client.get('/me/MailboxSettings')));
|
|
29
|
+
}, async () => minifiedResult(stripOData(await client.get('/me/MailboxSettings'))));
|
|
29
30
|
server.registerTool('outlook_list_contacts', {
|
|
30
31
|
description: 'List saved contacts from the mailbox address book.',
|
|
31
32
|
annotations: { readOnlyHint: true },
|
|
@@ -38,7 +39,9 @@ export function registerDirectoryTools(server, client) {
|
|
|
38
39
|
const data = await client.get(`/me/contacts${qs({
|
|
39
40
|
$top: limit ?? 50,
|
|
40
41
|
$skip: skip,
|
|
41
|
-
|
|
42
|
+
// `MobilePhone1`, not `MobilePhone`: the latter is the Graph name and
|
|
43
|
+
// the v2.0 Contact type rejects it outright with a 400.
|
|
44
|
+
$select: 'Id,DisplayName,EmailAddresses,CompanyName,JobTitle,MobilePhone1',
|
|
42
45
|
})}`);
|
|
43
46
|
return minifiedResult(collection(data, resolveView(view, VIEWS) === 'raw'));
|
|
44
47
|
});
|
package/dist/tools/mail.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { minifiedResult, resolveView, viewParam, McpToolError } from '@chrischall/mcp-utils';
|
|
3
|
-
import { compactFolder, compactMessage, fullMessage, projectCollection, } from '../view.js';
|
|
3
|
+
import { compactFolder, compactMessage, fullMessage, projectCollection, stripOData, } from '../view.js';
|
|
4
4
|
export const VIEWS = ['compact', 'full', 'raw'];
|
|
5
5
|
/**
|
|
6
6
|
* Well-known folder names the API accepts wherever a folder id is taken.
|
|
@@ -124,7 +124,7 @@ export function registerMailTools(server, client) {
|
|
|
124
124
|
})}`);
|
|
125
125
|
return minifiedResult({
|
|
126
126
|
count: data.value?.length ?? 0,
|
|
127
|
-
items: data.value ?? [],
|
|
127
|
+
items: (data.value ?? []).map(stripOData),
|
|
128
128
|
});
|
|
129
129
|
});
|
|
130
130
|
}
|
package/dist/tools/writes.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { minifiedResult } from '@chrischall/mcp-utils';
|
|
3
3
|
import { previewUnlessConfirmed, schemaConfirm } from './_confirm.js';
|
|
4
|
+
import { mailboxTimeZone } from '../timezone.js';
|
|
4
5
|
const recipientList = z
|
|
5
6
|
.array(z.string().min(3))
|
|
6
7
|
.optional()
|
|
@@ -133,14 +134,15 @@ export function registerWriteTools(server, client) {
|
|
|
133
134
|
timeZone: z
|
|
134
135
|
.string()
|
|
135
136
|
.optional()
|
|
136
|
-
.describe('Windows time-zone name
|
|
137
|
+
.describe('Windows time-zone name. Defaults to the MAILBOX time zone.'),
|
|
137
138
|
location: z.string().optional().describe('Location display name'),
|
|
138
139
|
body: z.string().optional().describe('Event description'),
|
|
139
140
|
attendees: recipientList,
|
|
140
141
|
confirm: schemaConfirm,
|
|
141
142
|
}),
|
|
142
143
|
}, async ({ subject, start, end, timeZone, location, body, attendees, confirm }) => {
|
|
143
|
-
|
|
144
|
+
// Falls back to UTC only when the mailbox itself declares no zone.
|
|
145
|
+
const tz = timeZone ?? (await mailboxTimeZone(client)) ?? 'UTC';
|
|
144
146
|
const payload = {
|
|
145
147
|
Subject: subject,
|
|
146
148
|
Start: { DateTime: start, TimeZone: tz },
|
package/dist/version.js
CHANGED
package/dist/view.js
CHANGED
|
@@ -83,6 +83,27 @@ export function compactFolder(f) {
|
|
|
83
83
|
Total: f.TotalItemCount,
|
|
84
84
|
});
|
|
85
85
|
}
|
|
86
|
+
/**
|
|
87
|
+
* Drop OData's envelope keys (`@odata.id`, `@odata.etag`, `@odata.context`, …)
|
|
88
|
+
* from a record.
|
|
89
|
+
*
|
|
90
|
+
* `$select` does not suppress them: a contact selected down to six fields still
|
|
91
|
+
* arrives with a ~200-char `@odata.id` self-URL repeating the mailbox GUID and
|
|
92
|
+
* the item Id, which on a 50-item listing is most of the payload. The typed
|
|
93
|
+
* projections never had this problem because they name their fields; the tools
|
|
94
|
+
* that pass records through untouched did.
|
|
95
|
+
*
|
|
96
|
+
* Everything that is not an annotation survives, so this stays a projection of
|
|
97
|
+
* the envelope rather than of the record.
|
|
98
|
+
*/
|
|
99
|
+
export function stripOData(o) {
|
|
100
|
+
const out = {};
|
|
101
|
+
for (const [k, v] of Object.entries(o)) {
|
|
102
|
+
if (!k.startsWith('@odata.'))
|
|
103
|
+
out[k] = v;
|
|
104
|
+
}
|
|
105
|
+
return out;
|
|
106
|
+
}
|
|
86
107
|
/** Drop `undefined` so a compact record does not carry empty keys. */
|
|
87
108
|
function pruned(o) {
|
|
88
109
|
const out = {};
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chrischall/office-outlook-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"mcpName": "io.github.chrischall/office-outlook-mcp",
|
|
5
|
-
"description": "Outlook / Microsoft 365 MCP server for Claude
|
|
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>",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
package/server.json
CHANGED
|
@@ -6,13 +6,15 @@
|
|
|
6
6
|
"url": "https://github.com/chrischall/office-outlook-mcp",
|
|
7
7
|
"source": "github"
|
|
8
8
|
},
|
|
9
|
-
"version": "0.
|
|
9
|
+
"version": "0.1.2",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "@chrischall/office-outlook-mcp",
|
|
14
|
-
"version": "0.
|
|
15
|
-
"transport": {
|
|
14
|
+
"version": "0.1.2",
|
|
15
|
+
"transport": {
|
|
16
|
+
"type": "stdio"
|
|
17
|
+
},
|
|
16
18
|
"environmentVariables": [
|
|
17
19
|
{
|
|
18
20
|
"name": "OUTLOOK_ACCESS_TOKEN",
|