@chrischall/office-outlook-mcp 0.3.4 → 0.4.0
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 +12 -3
- package/dist/bundle.js +440 -27
- package/dist/client.js +13 -0
- package/dist/tools/calendar.js +164 -2
- package/dist/tools/writes.js +222 -14
- package/dist/version.js +1 -1
- package/dist/view.js +5 -1
- package/package.json +3 -3
- package/server.json +2 -2
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
},
|
|
8
8
|
"metadata": {
|
|
9
9
|
"description": "MCP server for Outlook / Microsoft 365 — mail, calendar, contacts and tasks via natural language",
|
|
10
|
-
"version": "0.
|
|
10
|
+
"version": "0.4.0"
|
|
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 — read mail, folders, calendar, contacts and tasks, and send mail with confirmation",
|
|
18
|
-
"version": "0.
|
|
18
|
+
"version": "0.4.0",
|
|
19
19
|
"author": {
|
|
20
20
|
"name": "Chris Hall"
|
|
21
21
|
},
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "office-outlook-mcp",
|
|
3
3
|
"displayName": "Outlook (Microsoft 365)",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.4.0",
|
|
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",
|
package/README.md
CHANGED
|
@@ -108,13 +108,21 @@ Everything is optional — with nothing set, the server captures from the browse
|
|
|
108
108
|
|
|
109
109
|
**Read** — `outlook_list_folders`, `outlook_list_messages`,
|
|
110
110
|
`outlook_get_message`, `outlook_list_attachments`, `outlook_list_events`,
|
|
111
|
-
`outlook_get_event`, `outlook_list_calendars`, `
|
|
111
|
+
`outlook_get_event`, `outlook_list_calendars`, `outlook_find_meeting_times`,
|
|
112
|
+
`outlook_get_schedule`, `outlook_get_profile`,
|
|
112
113
|
`outlook_get_mailbox_settings`, `outlook_list_contacts`, `outlook_list_people`,
|
|
113
114
|
`outlook_list_tasks`
|
|
114
115
|
|
|
115
116
|
**Write** (all ask you to confirm first — see [Confirmations](#confirmations)) — `outlook_send_mail`,
|
|
116
117
|
`outlook_create_draft`, `outlook_mark_read`, `outlook_move_message`,
|
|
117
|
-
`outlook_create_event`
|
|
118
|
+
`outlook_create_event`, `outlook_update_event`
|
|
119
|
+
|
|
120
|
+
**Meetings** — `outlook_find_meeting_times` asks Outlook's Scheduling
|
|
121
|
+
Assistant for slots when everyone is free, and `outlook_get_schedule` shows
|
|
122
|
+
each person's busy blocks. `outlook_create_event` and `outlook_update_event`
|
|
123
|
+
attach a Microsoft Teams meeting by default and return its join link; pass
|
|
124
|
+
`teamsMeeting: false` to leave it off. Only the organizer can update a
|
|
125
|
+
meeting.
|
|
118
126
|
|
|
119
127
|
**Diagnostics** — `outlook_healthcheck`
|
|
120
128
|
|
|
@@ -148,7 +156,8 @@ a fresh preview.
|
|
|
148
156
|
|
|
149
157
|
| variable | default | |
|
|
150
158
|
|---|---|---|
|
|
151
|
-
| `MCP_CONFIRM_MODE` | `ask-user` | What a write does on a client that cannot show a confirmation prompt (claude.ai, Claude Desktop). `ask-user`: two steps — the first call does nothing and returns a preview plus a token, and the model must get your approval in chat before calling again with it. `auto`: the same two steps, but the model may use the token after reviewing the preview itself. `refuse`: writes are refused on such clients. A client that can show prompts (Claude Code) always gets the real prompt
|
|
159
|
+
| `MCP_CONFIRM_MODE` | `ask-user` | What a write does on a client that cannot show a confirmation prompt (claude.ai, Claude Desktop). `ask-user`: two steps — the first call does nothing and returns a preview plus a token, and the model must get your approval in chat before calling again with it. `auto`: the same two steps, but the model may use the token after reviewing the preview itself. `refuse`: writes are refused on such clients. A client that can show prompts (Claude Code) always gets the real prompt, unless `MCP_CONFIRM_ELICITATION=off`. An unrecognised value is treated as `refuse`. |
|
|
160
|
+
| `MCP_CONFIRM_ELICITATION` | `on` | `off` never shows a confirmation prompt, so every client gets the `MCP_CONFIRM_MODE` behaviour. Set it for a client that says it can show prompts but never does (the write hangs — opencode 2.0.x). Any other value is treated as `on`, with a warning on stderr. |
|
|
152
161
|
| `MCP_CONFIRM_TTL_SECONDS` | `600` | How long a token stays valid. |
|
|
153
162
|
| `MCP_CONFIRM_SECRET` | random per process | Signing key; set it only if tokens must survive a server restart. On mcp-host the host supplies a stable per-child key (`MCP_HOST_CONFIRM_SECRET`) and spent tokens are recorded under `MCP_DATA_DIR`, so an approval survives an idle restart. |
|
|
154
163
|
|
package/dist/bundle.js
CHANGED
|
@@ -23737,10 +23737,10 @@ function scanXMcpHeaderDeclarations(inputSchema) {
|
|
|
23737
23737
|
if (!RFC9110_TOKEN.test(raw)) return `${pathName(path)}: x-mcp-header '${raw}' is not a valid RFC 9110 token (no spaces, control characters or HTTP delimiters)`;
|
|
23738
23738
|
const type = typeof schema.type === "string" ? schema.type : void 0;
|
|
23739
23739
|
if (type === void 0 || !PERMITTED_X_MCP_HEADER_TYPES.has(type)) return `${pathName(path)}: x-mcp-header is only permitted on primitive-typed properties (string, integer, boolean); got ${type ?? "<none>"}`;
|
|
23740
|
-
const
|
|
23741
|
-
const prior = seenLower.get(
|
|
23740
|
+
const lower2 = raw.toLowerCase();
|
|
23741
|
+
const prior = seenLower.get(lower2);
|
|
23742
23742
|
if (prior !== void 0) return `x-mcp-header '${raw}' is not case-insensitively unique (also declared as '${prior}')`;
|
|
23743
|
-
seenLower.set(
|
|
23743
|
+
seenLower.set(lower2, raw);
|
|
23744
23744
|
declarations.push({
|
|
23745
23745
|
path,
|
|
23746
23746
|
headerName: raw,
|
|
@@ -35663,6 +35663,15 @@ ${JSON.stringify(preview, null, 2)}`,
|
|
|
35663
35663
|
};
|
|
35664
35664
|
const response = inputResponse(ctx.mcpReq.inputResponses, requestKey);
|
|
35665
35665
|
const accepted = acceptedContent(ctx.mcpReq.inputResponses, requestKey, confirmationSchema);
|
|
35666
|
+
if (options.elicitation === false) {
|
|
35667
|
+
return textResult({
|
|
35668
|
+
confirmed: false,
|
|
35669
|
+
dispatched: false,
|
|
35670
|
+
action: options.action,
|
|
35671
|
+
reason: "confirmation-unsupported",
|
|
35672
|
+
note: options.unsupportedNote ? `${DISABLED_NOTE}. ${options.unsupportedNote}` : `${DISABLED_NOTE}.`
|
|
35673
|
+
});
|
|
35674
|
+
}
|
|
35666
35675
|
if (response.kind === "missing") {
|
|
35667
35676
|
if (callerAcceptsFormElicitation(ctx) === false) {
|
|
35668
35677
|
return textResult({
|
|
@@ -35693,7 +35702,7 @@ ${JSON.stringify(preview, null, 2)}`,
|
|
|
35693
35702
|
note: "Nothing was changed because the confirmation was declined, cancelled, or left unchecked."
|
|
35694
35703
|
});
|
|
35695
35704
|
}
|
|
35696
|
-
var DEFAULT_REQUEST_KEY, DEFAULT_CONFIRMATION_LABEL, UNSUPPORTED_NOTE, STATE_PREFIX;
|
|
35705
|
+
var DEFAULT_REQUEST_KEY, DEFAULT_CONFIRMATION_LABEL, UNSUPPORTED_NOTE, DISABLED_NOTE, STATE_PREFIX;
|
|
35697
35706
|
var init_confirmation = __esm({
|
|
35698
35707
|
"node_modules/@chrischall/mcp-utils/dist/server/confirmation.js"() {
|
|
35699
35708
|
init_dist();
|
|
@@ -35704,6 +35713,7 @@ var init_confirmation = __esm({
|
|
|
35704
35713
|
DEFAULT_REQUEST_KEY = "confirmation";
|
|
35705
35714
|
DEFAULT_CONFIRMATION_LABEL = "Confirm this action should proceed.";
|
|
35706
35715
|
UNSUPPORTED_NOTE = "Nothing was done because this client cannot show a confirmation prompt (it declares no MCP elicitation capability), and this action is never taken without one";
|
|
35716
|
+
DISABLED_NOTE = "Nothing was done because confirmation prompts are turned off for this server, and this action is never taken without one";
|
|
35707
35717
|
STATE_PREFIX = "mcpu.confirm.v1.";
|
|
35708
35718
|
}
|
|
35709
35719
|
});
|
|
@@ -35857,7 +35867,7 @@ async function tokenConfirmation(action, fb) {
|
|
|
35857
35867
|
}
|
|
35858
35868
|
async function requireConfirmationWithFallback(ctx, options) {
|
|
35859
35869
|
const { tokenFallback, ...confirmation } = options;
|
|
35860
|
-
if (tokenFallback && callerAcceptsFormElicitation(ctx) === false) {
|
|
35870
|
+
if (tokenFallback && (options.elicitation === false || callerAcceptsFormElicitation(ctx) === false)) {
|
|
35861
35871
|
return tokenConfirmation(options.action, tokenFallback);
|
|
35862
35872
|
}
|
|
35863
35873
|
return requireConfirmation(ctx, confirmation);
|
|
@@ -36068,6 +36078,20 @@ function readConfirmMode(env = process.env) {
|
|
|
36068
36078
|
}
|
|
36069
36079
|
return "refuse";
|
|
36070
36080
|
}
|
|
36081
|
+
function readConfirmElicitation(env = process.env) {
|
|
36082
|
+
const raw = readEnvVar("MCP_CONFIRM_ELICITATION", { env })?.trim().toLowerCase();
|
|
36083
|
+
if (!raw || raw === "on")
|
|
36084
|
+
return "on";
|
|
36085
|
+
if (raw === "off")
|
|
36086
|
+
return "off";
|
|
36087
|
+
const key = `elicitation:${raw}`;
|
|
36088
|
+
if (!warned.has(key)) {
|
|
36089
|
+
warned.add(key);
|
|
36090
|
+
process.stderr.write(`MCP_CONFIRM_ELICITATION="${raw}" is not one of on, off; treating it as on.
|
|
36091
|
+
`);
|
|
36092
|
+
}
|
|
36093
|
+
return "on";
|
|
36094
|
+
}
|
|
36071
36095
|
function readConfirmTtl(env) {
|
|
36072
36096
|
const raw = readEnvVar("MCP_CONFIRM_TTL_SECONDS", { env })?.trim();
|
|
36073
36097
|
if (!raw)
|
|
@@ -36120,10 +36144,13 @@ function confirmationFromEnv(options) {
|
|
|
36120
36144
|
const spent = callerSpent ?? spentTokenStoreFromEnv(env);
|
|
36121
36145
|
const mode = readConfirmMode(env);
|
|
36122
36146
|
const ttlSeconds = readConfirmTtl(env);
|
|
36147
|
+
const elicitationOff = readConfirmElicitation(env) === "off";
|
|
36123
36148
|
const bound = args === void 0 ? void 0 : boundArgs(args);
|
|
36124
|
-
const
|
|
36149
|
+
const bindingAdded = bound === void 0 || rest.binding ? rest : { ...rest, binding: { key: confirmKeyFromEnv(env), args: bound, ttlSeconds: ttlSeconds ?? DEFAULT_TTL_SECONDS2 } };
|
|
36150
|
+
const confirmation = elicitationOff ? { ...bindingAdded, elicitation: false } : bindingAdded;
|
|
36125
36151
|
if (mode === "refuse" || ttlSeconds === void 0) {
|
|
36126
|
-
const
|
|
36152
|
+
const reason = mode === "refuse" ? REFUSE_HINT : BAD_TTL_HINT;
|
|
36153
|
+
const hint = elicitationOff ? `${ELICITATION_OFF_HINT} ${reason}` : reason;
|
|
36127
36154
|
return {
|
|
36128
36155
|
...confirmation,
|
|
36129
36156
|
unsupportedNote: confirmation.unsupportedNote ? `${confirmation.unsupportedNote} ${hint}` : hint
|
|
@@ -36149,7 +36176,7 @@ function confirmationFromEnv(options) {
|
|
|
36149
36176
|
}
|
|
36150
36177
|
};
|
|
36151
36178
|
}
|
|
36152
|
-
var MODES, DEFAULT_TTL_SECONDS2, warned, processKey, fileStores, CONFIRM_TOKEN_AUTO_INSTRUCTION, REFUSE_HINT, BAD_TTL_HINT;
|
|
36179
|
+
var MODES, DEFAULT_TTL_SECONDS2, warned, processKey, fileStores, CONFIRM_TOKEN_AUTO_INSTRUCTION, REFUSE_HINT, ELICITATION_OFF_HINT, BAD_TTL_HINT;
|
|
36153
36180
|
var init_confirm_env = __esm({
|
|
36154
36181
|
"node_modules/@chrischall/mcp-utils/dist/server/confirm-env.js"() {
|
|
36155
36182
|
init_config();
|
|
@@ -36161,6 +36188,7 @@ var init_confirm_env = __esm({
|
|
|
36161
36188
|
fileStores = /* @__PURE__ */ new Map();
|
|
36162
36189
|
CONFIRM_TOKEN_AUTO_INSTRUCTION = "Nothing has been done yet. Review this preview; if it is what was intended, call again with the same arguments plus confirmToken. (This server runs with MCP_CONFIRM_MODE=auto, so the user's approval in chat is not required.)";
|
|
36163
36190
|
REFUSE_HINT = "Set MCP_CONFIRM_MODE=ask-user on the server to allow two-step confirmation instead.";
|
|
36191
|
+
ELICITATION_OFF_HINT = "MCP_CONFIRM_ELICITATION=off on the server turns confirmation prompts off.";
|
|
36164
36192
|
BAD_TTL_HINT = "MCP_CONFIRM_TTL_SECONDS on the server is not a positive whole number of seconds; fix it to allow two-step confirmation.";
|
|
36165
36193
|
}
|
|
36166
36194
|
});
|
|
@@ -37120,7 +37148,7 @@ var init_version = __esm({
|
|
|
37120
37148
|
"src/version.ts"() {
|
|
37121
37149
|
"use strict";
|
|
37122
37150
|
PACKAGE_NAME = "office-outlook-mcp";
|
|
37123
|
-
VERSION = "0.
|
|
37151
|
+
VERSION = "0.4.0";
|
|
37124
37152
|
}
|
|
37125
37153
|
});
|
|
37126
37154
|
|
|
@@ -37372,7 +37400,7 @@ async function transcriptHash(mcpNonce, extNonce, mcpSessionPub, extSessionPub)
|
|
|
37372
37400
|
function frameAad(mcpId, seq, direction) {
|
|
37373
37401
|
return enc.encode(`fetchproxy/4/frame\0${mcpId}\0${seq}\0${direction}`);
|
|
37374
37402
|
}
|
|
37375
|
-
var PROTOCOL_VERSION, HKDF_SESSION_INFO, enc, ANSWERS_NO_EXT_SESSION, KNOWN_CAPABILITIES, ACCOUNT_KEY_FRAME, ACCOUNT_ATTEST_FRAME;
|
|
37403
|
+
var PROTOCOL_VERSION, HKDF_SESSION_INFO, enc, ANSWERS_NO_EXT_SESSION, KNOWN_CAPABILITIES, ACCOUNT_KEY_FRAME, ACCOUNT_ATTEST_FRAME, BRIDGE_ROLE_FRAME, BRIDGE_SERVE_FRAME, ROOM_PING_FRAME, ROOM_PONG_FRAME, BRIDGE_ROLE_LABEL_MAX, ROOM_FRAME_ACCEPTS;
|
|
37376
37404
|
var init_frames = __esm({
|
|
37377
37405
|
"node_modules/@fetchproxy/protocol/dist/frames.js"() {
|
|
37378
37406
|
init_crypto();
|
|
@@ -37398,6 +37426,17 @@ var init_frames = __esm({
|
|
|
37398
37426
|
]);
|
|
37399
37427
|
ACCOUNT_KEY_FRAME = "account-key";
|
|
37400
37428
|
ACCOUNT_ATTEST_FRAME = "account-attest";
|
|
37429
|
+
BRIDGE_ROLE_FRAME = "bridge-role";
|
|
37430
|
+
BRIDGE_SERVE_FRAME = "bridge-serve";
|
|
37431
|
+
ROOM_PING_FRAME = "room-ping";
|
|
37432
|
+
ROOM_PONG_FRAME = "room-pong";
|
|
37433
|
+
BRIDGE_ROLE_LABEL_MAX = 64;
|
|
37434
|
+
ROOM_FRAME_ACCEPTS = Object.freeze({
|
|
37435
|
+
[BRIDGE_ROLE_FRAME]: BRIDGE_ROLE_FRAME,
|
|
37436
|
+
[BRIDGE_SERVE_FRAME]: BRIDGE_SERVE_FRAME,
|
|
37437
|
+
[ROOM_PING_FRAME]: ROOM_PING_FRAME,
|
|
37438
|
+
[ROOM_PONG_FRAME]: ROOM_PING_FRAME
|
|
37439
|
+
});
|
|
37401
37440
|
}
|
|
37402
37441
|
});
|
|
37403
37442
|
|
|
@@ -38081,6 +38120,12 @@ function validateFrame(raw) {
|
|
|
38081
38120
|
return validateAccountKey(raw);
|
|
38082
38121
|
if (t === ACCOUNT_ATTEST_FRAME)
|
|
38083
38122
|
return validateAccountAttest(raw);
|
|
38123
|
+
if (t === BRIDGE_ROLE_FRAME)
|
|
38124
|
+
return validateBridgeRole(raw);
|
|
38125
|
+
if (t === BRIDGE_SERVE_FRAME || t === ROOM_PING_FRAME || t === ROOM_PONG_FRAME) {
|
|
38126
|
+
assertExactFields(raw, TYPE_ONLY_FIELDS, t);
|
|
38127
|
+
return { type: t };
|
|
38128
|
+
}
|
|
38084
38129
|
throw new ProtocolError3(`unknown frame type: ${String(t)}`);
|
|
38085
38130
|
}
|
|
38086
38131
|
function validateHello(raw) {
|
|
@@ -38376,6 +38421,40 @@ function validateAccountAttest(raw) {
|
|
|
38376
38421
|
sig: raw.sig
|
|
38377
38422
|
};
|
|
38378
38423
|
}
|
|
38424
|
+
function validateBridgeRole(raw) {
|
|
38425
|
+
const L = BRIDGE_ROLE_FRAME;
|
|
38426
|
+
const role = raw.role;
|
|
38427
|
+
if (role !== "serving" && role !== "standby") {
|
|
38428
|
+
throw new ProtocolError3(`${L}.role: must be one of serving, standby`);
|
|
38429
|
+
}
|
|
38430
|
+
const canServe = raw.canServe;
|
|
38431
|
+
assertBoolean(canServe, `${L}.canServe`);
|
|
38432
|
+
assertExactFields(raw, BRIDGE_ROLE_FIELDS, L);
|
|
38433
|
+
const serving = raw.serving;
|
|
38434
|
+
if (role === "serving") {
|
|
38435
|
+
if (serving !== void 0) {
|
|
38436
|
+
throw new ProtocolError3(`${L}.serving: must be absent when role is serving`);
|
|
38437
|
+
}
|
|
38438
|
+
return { type: BRIDGE_ROLE_FRAME, role, canServe };
|
|
38439
|
+
}
|
|
38440
|
+
if (serving === void 0) {
|
|
38441
|
+
throw new ProtocolError3(`${L}.serving: required when role is standby`);
|
|
38442
|
+
}
|
|
38443
|
+
assertObject(serving, `${L}.serving`);
|
|
38444
|
+
const label = serving.label;
|
|
38445
|
+
assertDisplayString(label, `${L}.serving.label`, 1, BRIDGE_ROLE_LABEL_MAX);
|
|
38446
|
+
const since = serving.since;
|
|
38447
|
+
if (typeof since !== "number" || !Number.isSafeInteger(since) || since < 0) {
|
|
38448
|
+
throw new ProtocolError3(`${L}.serving.since: expected non-negative safe integer (Unix ms)`);
|
|
38449
|
+
}
|
|
38450
|
+
assertExactFields(serving, BRIDGE_ROLE_SERVING_FIELDS, `${L}.serving`);
|
|
38451
|
+
return {
|
|
38452
|
+
type: BRIDGE_ROLE_FRAME,
|
|
38453
|
+
role,
|
|
38454
|
+
canServe,
|
|
38455
|
+
serving: { label, since }
|
|
38456
|
+
};
|
|
38457
|
+
}
|
|
38379
38458
|
function validateInnerFrame(raw) {
|
|
38380
38459
|
assertObject(raw, "inner");
|
|
38381
38460
|
const t = raw.type;
|
|
@@ -38837,7 +38916,7 @@ function validateInnerResponse(raw) {
|
|
|
38837
38916
|
}
|
|
38838
38917
|
throw new ProtocolError3(`inner.ok: must be boolean, got ${typeof raw.ok}`);
|
|
38839
38918
|
}
|
|
38840
|
-
var ProtocolError3, FORBIDDEN_KEYS, BASE64_RE, SCOPE_KEY_RE, SCOPE_KEY_GLOB_RE, HEADER_NAME_RE, HOSTNAME_RE, CAPTURE_PATH_RE, DOM_SELECTOR_RE, DOM_ATTRIBUTE_RE, GRAPHQL_OP_NAME_RE, PAIR_CODE_RE, DISPLAY_FORBIDDEN_RE, ACCOUNT_KEY_FIELDS, ACCOUNT_ATTEST_FIELDS, KNOWN_RESPONSE_OPS;
|
|
38919
|
+
var ProtocolError3, FORBIDDEN_KEYS, BASE64_RE, SCOPE_KEY_RE, SCOPE_KEY_GLOB_RE, HEADER_NAME_RE, HOSTNAME_RE, CAPTURE_PATH_RE, DOM_SELECTOR_RE, DOM_ATTRIBUTE_RE, GRAPHQL_OP_NAME_RE, PAIR_CODE_RE, DISPLAY_FORBIDDEN_RE, ACCOUNT_KEY_FIELDS, ACCOUNT_ATTEST_FIELDS, TYPE_ONLY_FIELDS, BRIDGE_ROLE_FIELDS, BRIDGE_ROLE_SERVING_FIELDS, KNOWN_RESPONSE_OPS;
|
|
38841
38920
|
var init_validate = __esm({
|
|
38842
38921
|
"node_modules/@fetchproxy/protocol/dist/validate.js"() {
|
|
38843
38922
|
init_frames();
|
|
@@ -38892,6 +38971,9 @@ var init_validate = __esm({
|
|
|
38892
38971
|
"notAfter",
|
|
38893
38972
|
"sig"
|
|
38894
38973
|
];
|
|
38974
|
+
TYPE_ONLY_FIELDS = ["type"];
|
|
38975
|
+
BRIDGE_ROLE_FIELDS = ["type", "role", "canServe", "serving"];
|
|
38976
|
+
BRIDGE_ROLE_SERVING_FIELDS = ["label", "since"];
|
|
38895
38977
|
KNOWN_RESPONSE_OPS = /* @__PURE__ */ new Set([
|
|
38896
38978
|
...KNOWN_CAPABILITIES,
|
|
38897
38979
|
"graphql_query"
|
|
@@ -38899,6 +38981,14 @@ var init_validate = __esm({
|
|
|
38899
38981
|
}
|
|
38900
38982
|
});
|
|
38901
38983
|
|
|
38984
|
+
// node_modules/@fetchproxy/protocol/dist/room-frames.js
|
|
38985
|
+
var init_room_frames = __esm({
|
|
38986
|
+
"node_modules/@fetchproxy/protocol/dist/room-frames.js"() {
|
|
38987
|
+
init_frames();
|
|
38988
|
+
init_validate();
|
|
38989
|
+
}
|
|
38990
|
+
});
|
|
38991
|
+
|
|
38902
38992
|
// node_modules/@fetchproxy/protocol/dist/pair-code.js
|
|
38903
38993
|
async function pairTranscript(mcpIdentityPub, extIdentityPub, mcpNonce, extNonce, mcpSessionPub) {
|
|
38904
38994
|
const label = enc3.encode(PAIR_LABEL);
|
|
@@ -39007,6 +39097,7 @@ var init_dist3 = __esm({
|
|
|
39007
39097
|
init_awaiting_approval();
|
|
39008
39098
|
init_capability_availability();
|
|
39009
39099
|
init_validate();
|
|
39100
|
+
init_room_frames();
|
|
39010
39101
|
init_crypto();
|
|
39011
39102
|
init_mcp_id();
|
|
39012
39103
|
init_pair_code();
|
|
@@ -41405,11 +41496,23 @@ var require_websocket = __commonJS({
|
|
|
41405
41496
|
this._isServer = false;
|
|
41406
41497
|
this._redirects = 0;
|
|
41407
41498
|
if (protocols === void 0) {
|
|
41408
|
-
protocols
|
|
41499
|
+
if (!options || options.protocols === void 0) {
|
|
41500
|
+
protocols = [];
|
|
41501
|
+
} else if (Array.isArray(options.protocols)) {
|
|
41502
|
+
protocols = options.protocols;
|
|
41503
|
+
} else {
|
|
41504
|
+
protocols = [options.protocols];
|
|
41505
|
+
}
|
|
41409
41506
|
} else if (!Array.isArray(protocols)) {
|
|
41410
41507
|
if (typeof protocols === "object" && protocols !== null) {
|
|
41411
41508
|
options = protocols;
|
|
41412
|
-
protocols
|
|
41509
|
+
if (options.protocols === void 0) {
|
|
41510
|
+
protocols = [];
|
|
41511
|
+
} else if (Array.isArray(options.protocols)) {
|
|
41512
|
+
protocols = options.protocols;
|
|
41513
|
+
} else {
|
|
41514
|
+
protocols = [options.protocols];
|
|
41515
|
+
}
|
|
41413
41516
|
} else {
|
|
41414
41517
|
protocols = [protocols];
|
|
41415
41518
|
}
|
|
@@ -41606,7 +41709,6 @@ var require_websocket = __commonJS({
|
|
|
41606
41709
|
}
|
|
41607
41710
|
return;
|
|
41608
41711
|
}
|
|
41609
|
-
this._readyState = _WebSocket.CLOSING;
|
|
41610
41712
|
this._sender.close(code2, data, !this._isServer, (err) => {
|
|
41611
41713
|
if (err) return;
|
|
41612
41714
|
this._closeFrameSent = true;
|
|
@@ -41614,6 +41716,7 @@ var require_websocket = __commonJS({
|
|
|
41614
41716
|
this._socket.end();
|
|
41615
41717
|
}
|
|
41616
41718
|
});
|
|
41719
|
+
this._readyState = _WebSocket.CLOSING;
|
|
41617
41720
|
setCloseTimer(this);
|
|
41618
41721
|
}
|
|
41619
41722
|
/**
|
|
@@ -41838,6 +41941,7 @@ var require_websocket = __commonJS({
|
|
|
41838
41941
|
socketPath: void 0,
|
|
41839
41942
|
hostname: void 0,
|
|
41840
41943
|
protocol: void 0,
|
|
41944
|
+
protocols: void 0,
|
|
41841
41945
|
timeout: void 0,
|
|
41842
41946
|
method: "GET",
|
|
41843
41947
|
host: void 0,
|
|
@@ -48283,6 +48387,19 @@ var OutlookClient = class {
|
|
|
48283
48387
|
}
|
|
48284
48388
|
return this.get(target.pathname.slice(prefix.length) + target.search, opts);
|
|
48285
48389
|
}
|
|
48390
|
+
/**
|
|
48391
|
+
* POST a read-only query — `findmeetingtimes`, `getschedule` — that changes
|
|
48392
|
+
* nothing in the mailbox. Kept apart from `write` so a reader can tell the
|
|
48393
|
+
* two apart at the call site, and because these take `Prefer` (notably
|
|
48394
|
+
* `outlook.timezone`, without which every returned slot is UTC).
|
|
48395
|
+
*/
|
|
48396
|
+
async post(path, body, opts = {}) {
|
|
48397
|
+
this.#requireConfigured();
|
|
48398
|
+
return this.#client().fetchJson("POST", path, {
|
|
48399
|
+
body,
|
|
48400
|
+
headers: opts.prefer ? { Prefer: opts.prefer } : void 0
|
|
48401
|
+
});
|
|
48402
|
+
}
|
|
48286
48403
|
/**
|
|
48287
48404
|
* The single write path. Everything mutating goes through here so auth and
|
|
48288
48405
|
* error shaping stay in one place.
|
|
@@ -48383,7 +48500,11 @@ function fullEvent(e) {
|
|
|
48383
48500
|
Attendees: e.Attendees?.map(
|
|
48384
48501
|
(a) => pruned({ Who: addr(a), Response: a.Status?.Response })
|
|
48385
48502
|
),
|
|
48386
|
-
OnlineMeetingUrl
|
|
48503
|
+
// The Teams link lives under OnlineMeeting; OnlineMeetingUrl is empty on
|
|
48504
|
+
// every live Teams meeting checked.
|
|
48505
|
+
JoinUrl: e.OnlineMeeting?.JoinUrl || e.OnlineMeetingUrl || void 0,
|
|
48506
|
+
// Kept beside JoinUrl so a consumer of the old field still finds it.
|
|
48507
|
+
OnlineMeetingUrl: e.OnlineMeetingUrl || void 0,
|
|
48387
48508
|
Preview: e.BodyPreview?.trim() || void 0,
|
|
48388
48509
|
WebLink: e.WebLink
|
|
48389
48510
|
});
|
|
@@ -48599,7 +48720,7 @@ async function mailboxTimeZone(client2) {
|
|
|
48599
48720
|
}
|
|
48600
48721
|
|
|
48601
48722
|
// src/tools/calendar.ts
|
|
48602
|
-
var EVENT_SELECT = "Id,Subject,Start,End,Location,Organizer,IsAllDay,IsCancelled,ShowAs,OnlineMeetingUrl,BodyPreview";
|
|
48723
|
+
var EVENT_SELECT = "Id,Subject,Start,End,Location,Organizer,IsAllDay,IsCancelled,ShowAs,OnlineMeetingUrl,OnlineMeeting,BodyPreview";
|
|
48603
48724
|
function qs2(params) {
|
|
48604
48725
|
const parts = [];
|
|
48605
48726
|
for (const [k, v] of Object.entries(params)) {
|
|
@@ -48608,6 +48729,28 @@ function qs2(params) {
|
|
|
48608
48729
|
}
|
|
48609
48730
|
return parts.length ? `?${parts.join("&")}` : "";
|
|
48610
48731
|
}
|
|
48732
|
+
function dateTimeTimeZone(value, zone) {
|
|
48733
|
+
if (/(Z|[+-]\d{2}:?\d{2})$/i.test(value)) {
|
|
48734
|
+
const d = new Date(value);
|
|
48735
|
+
if (!Number.isNaN(d.getTime())) {
|
|
48736
|
+
return { DateTime: d.toISOString().replace(/\.\d{3}Z$/, ""), TimeZone: "UTC" };
|
|
48737
|
+
}
|
|
48738
|
+
}
|
|
48739
|
+
return { DateTime: value, TimeZone: zone };
|
|
48740
|
+
}
|
|
48741
|
+
function assertWindow(start, end) {
|
|
48742
|
+
const a = Date.parse(start);
|
|
48743
|
+
const b = Date.parse(end);
|
|
48744
|
+
if (!Number.isNaN(a) && !Number.isNaN(b) && b <= a) {
|
|
48745
|
+
throw new McpToolError(`The window ends (${end}) before it starts (${start}).`, {
|
|
48746
|
+
hint: "`end` must be later than `start`."
|
|
48747
|
+
});
|
|
48748
|
+
}
|
|
48749
|
+
}
|
|
48750
|
+
function trimTime(dt) {
|
|
48751
|
+
return dt?.replace(/\.0+$/, "");
|
|
48752
|
+
}
|
|
48753
|
+
var attendeeList = (what) => external_exports.array(external_exports.string().min(3)).describe(`Email addresses of ${what}`);
|
|
48611
48754
|
function registerCalendarTools(server, client2) {
|
|
48612
48755
|
server.registerTool(
|
|
48613
48756
|
"outlook_list_events",
|
|
@@ -48684,6 +48827,119 @@ function registerCalendarTools(server, client2) {
|
|
|
48684
48827
|
return minifiedResult(plainCollection(data));
|
|
48685
48828
|
}
|
|
48686
48829
|
);
|
|
48830
|
+
server.registerTool(
|
|
48831
|
+
"outlook_find_meeting_times",
|
|
48832
|
+
{
|
|
48833
|
+
description: "Find times when people can meet \u2014 Outlook's Scheduling Assistant. Give the attendees, a search window and a duration; Outlook checks everyone's free/busy (and the signed-in user's, as organizer) and returns ranked candidate slots with each attendee's availability. By default only working hours are searched. Use this to answer \"when can X and Y meet?\"; then book with outlook_create_event. `start`/`end` are local wall-clock times in `timeZone` (default: the mailbox zone), and returned slots are in that zone too. " + UNTRUSTED_DESCRIPTION_SUFFIX,
|
|
48834
|
+
annotations: { readOnlyHint: true },
|
|
48835
|
+
inputSchema: external_exports.object({
|
|
48836
|
+
view: viewParam(VIEWS2),
|
|
48837
|
+
attendees: attendeeList("required attendees").min(1),
|
|
48838
|
+
optionalAttendees: attendeeList("optional attendees").optional(),
|
|
48839
|
+
start: external_exports.string().min(1).describe("Earliest the meeting may start, e.g. 2026-10-12T09:00:00"),
|
|
48840
|
+
end: external_exports.string().min(1).describe("Latest the meeting may end, e.g. 2026-10-16T17:00:00"),
|
|
48841
|
+
durationMinutes: external_exports.number().int().min(5).max(1440).describe("Meeting length in minutes"),
|
|
48842
|
+
timeZone: external_exports.string().optional().describe('Windows time-zone name, e.g. "Eastern Standard Time". Defaults to the MAILBOX time zone.'),
|
|
48843
|
+
maxCandidates: external_exports.number().int().min(1).max(50).optional().describe("Max suggestions (default 10)"),
|
|
48844
|
+
workingHoursOnly: external_exports.boolean().optional().describe("Only suggest times inside working hours (default true). false searches around the clock."),
|
|
48845
|
+
minimumAttendeePercentage: external_exports.number().min(0).max(100).optional().describe("Lowest confidence (0-100) a slot needs to be suggested. Lower it to see slots where not everyone is free."),
|
|
48846
|
+
isOrganizerOptional: external_exports.boolean().optional().describe("true when the signed-in user need not attend, so their own calendar is ignored")
|
|
48847
|
+
})
|
|
48848
|
+
},
|
|
48849
|
+
async (args) => {
|
|
48850
|
+
assertWindow(args.start, args.end);
|
|
48851
|
+
const zone = args.timeZone ?? await mailboxTimeZone(client2) ?? "UTC";
|
|
48852
|
+
const body = {
|
|
48853
|
+
Attendees: [
|
|
48854
|
+
...args.attendees.map((Address) => ({ Type: "Required", EmailAddress: { Address } })),
|
|
48855
|
+
...(args.optionalAttendees ?? []).map((Address) => ({
|
|
48856
|
+
Type: "Optional",
|
|
48857
|
+
EmailAddress: { Address }
|
|
48858
|
+
}))
|
|
48859
|
+
],
|
|
48860
|
+
TimeConstraint: {
|
|
48861
|
+
ActivityDomain: args.workingHoursOnly === false ? "Unrestricted" : "Work",
|
|
48862
|
+
Timeslots: [
|
|
48863
|
+
{ Start: dateTimeTimeZone(args.start, zone), End: dateTimeTimeZone(args.end, zone) }
|
|
48864
|
+
]
|
|
48865
|
+
},
|
|
48866
|
+
MeetingDuration: `PT${args.durationMinutes}M`,
|
|
48867
|
+
MaxCandidates: args.maxCandidates ?? 10,
|
|
48868
|
+
ReturnSuggestionReasons: true,
|
|
48869
|
+
...args.minimumAttendeePercentage !== void 0 ? { MinimumAttendeePercentage: args.minimumAttendeePercentage } : {},
|
|
48870
|
+
...args.isOrganizerOptional !== void 0 ? { IsOrganizerOptional: args.isOrganizerOptional } : {}
|
|
48871
|
+
};
|
|
48872
|
+
const data = await client2.post("/me/findmeetingtimes", body, { prefer: `outlook.timezone="${zone}"` });
|
|
48873
|
+
if (resolveView(args.view, VIEWS2) === "raw") return mailboxUntrusted(data);
|
|
48874
|
+
const suggestions = (data?.MeetingTimeSuggestions ?? []).map((s) => ({
|
|
48875
|
+
Start: trimTime(s.MeetingTimeSlot?.Start?.DateTime),
|
|
48876
|
+
End: trimTime(s.MeetingTimeSlot?.End?.DateTime),
|
|
48877
|
+
Confidence: s.Confidence,
|
|
48878
|
+
Organizer: s.OrganizerAvailability,
|
|
48879
|
+
Attendees: Object.fromEntries(
|
|
48880
|
+
(s.AttendeeAvailability ?? []).map((a) => [
|
|
48881
|
+
a.Attendee?.EmailAddress?.Address ?? a.Attendee?.EmailAddress?.Name ?? "?",
|
|
48882
|
+
a.Availability
|
|
48883
|
+
])
|
|
48884
|
+
),
|
|
48885
|
+
Reason: s.SuggestionReason
|
|
48886
|
+
}));
|
|
48887
|
+
return mailboxUntrusted({
|
|
48888
|
+
timeZone: zone,
|
|
48889
|
+
count: suggestions.length,
|
|
48890
|
+
suggestions,
|
|
48891
|
+
// Outlook explains an empty result ("AttendeesUnavailable", ...); an
|
|
48892
|
+
// empty list without it reads as "nobody is ever free".
|
|
48893
|
+
...data?.EmptySuggestionsReason ? { emptySuggestionsReason: data.EmptySuggestionsReason } : {}
|
|
48894
|
+
});
|
|
48895
|
+
}
|
|
48896
|
+
);
|
|
48897
|
+
server.registerTool(
|
|
48898
|
+
"outlook_get_schedule",
|
|
48899
|
+
{
|
|
48900
|
+
description: "Get people's free/busy blocks for a time window \u2014 the grid view of Outlook's Scheduling Assistant. Returns each person's busy, tentative and out-of-office items (with subject and location where their calendar shares them). Use outlook_find_meeting_times to have Outlook pick slots instead. `start`/`end` are local wall-clock times in `timeZone` (default: the mailbox zone). " + UNTRUSTED_DESCRIPTION_SUFFIX,
|
|
48901
|
+
annotations: { readOnlyHint: true },
|
|
48902
|
+
inputSchema: external_exports.object({
|
|
48903
|
+
view: viewParam(VIEWS2),
|
|
48904
|
+
people: attendeeList("people (or rooms) to look up").min(1).max(100),
|
|
48905
|
+
start: external_exports.string().min(1).describe("Window start, e.g. 2026-10-12T09:00:00"),
|
|
48906
|
+
end: external_exports.string().min(1).describe("Window end, e.g. 2026-10-12T17:00:00"),
|
|
48907
|
+
timeZone: external_exports.string().optional().describe('Windows time-zone name, e.g. "Eastern Standard Time". Defaults to the MAILBOX time zone.'),
|
|
48908
|
+
intervalMinutes: external_exports.number().int().min(5).max(1440).optional().describe("Slot size for the raw view's AvailabilityView string (default 30)")
|
|
48909
|
+
})
|
|
48910
|
+
},
|
|
48911
|
+
async ({ view, people, start, end, timeZone, intervalMinutes }) => {
|
|
48912
|
+
assertWindow(start, end);
|
|
48913
|
+
const zone = timeZone ?? await mailboxTimeZone(client2) ?? "UTC";
|
|
48914
|
+
const data = await client2.post(
|
|
48915
|
+
"/me/calendar/getschedule",
|
|
48916
|
+
{
|
|
48917
|
+
Schedules: people,
|
|
48918
|
+
StartTime: dateTimeTimeZone(start, zone),
|
|
48919
|
+
EndTime: dateTimeTimeZone(end, zone),
|
|
48920
|
+
AvailabilityViewInterval: intervalMinutes ?? 30
|
|
48921
|
+
},
|
|
48922
|
+
{ prefer: `outlook.timezone="${zone}"` }
|
|
48923
|
+
);
|
|
48924
|
+
if (resolveView(view, VIEWS2) === "raw") return mailboxUntrusted(data);
|
|
48925
|
+
const schedules = (data?.value ?? []).map(
|
|
48926
|
+
(p) => (
|
|
48927
|
+
// An unresolvable address must not read as "free all day".
|
|
48928
|
+
p.Error !== void 0 && p.Error !== null ? { Who: p.ScheduleId, Error: p.Error } : {
|
|
48929
|
+
Who: p.ScheduleId,
|
|
48930
|
+
Busy: (p.ScheduleItems ?? []).map((i) => ({
|
|
48931
|
+
Start: trimTime(i.Start?.DateTime),
|
|
48932
|
+
End: trimTime(i.End?.DateTime),
|
|
48933
|
+
Status: i.Status,
|
|
48934
|
+
...i.Subject ? { Subject: i.Subject } : {},
|
|
48935
|
+
...i.Location ? { Location: i.Location } : {}
|
|
48936
|
+
}))
|
|
48937
|
+
}
|
|
48938
|
+
)
|
|
48939
|
+
);
|
|
48940
|
+
return mailboxUntrusted({ timeZone: zone, schedules });
|
|
48941
|
+
}
|
|
48942
|
+
);
|
|
48687
48943
|
}
|
|
48688
48944
|
|
|
48689
48945
|
// src/tools/directory.ts
|
|
@@ -48808,6 +49064,31 @@ function messageBody(subject, body, html) {
|
|
|
48808
49064
|
Body: { ContentType: html === true ? "HTML" : "Text", Content: body }
|
|
48809
49065
|
};
|
|
48810
49066
|
}
|
|
49067
|
+
var TEAMS = { IsOnlineMeeting: true, OnlineMeetingProvider: "TeamsForBusiness" };
|
|
49068
|
+
var teamsMeetingParam = external_exports.boolean().optional().describe("Attach a Microsoft Teams meeting (default true). false leaves it off.");
|
|
49069
|
+
var EVENT_STATE_SELECT = "Id,Subject,IsOrganizer,Start,End,Location,Attendees,IsOnlineMeeting,OnlineMeetingProvider,OnlineMeeting";
|
|
49070
|
+
function attendee(Address, Type) {
|
|
49071
|
+
return { Type, EmailAddress: { Address } };
|
|
49072
|
+
}
|
|
49073
|
+
var lower = (a) => a.EmailAddress?.Address?.toLowerCase();
|
|
49074
|
+
function joinUrlOf(e) {
|
|
49075
|
+
return e?.OnlineMeeting?.JoinUrl || void 0;
|
|
49076
|
+
}
|
|
49077
|
+
function isTeams(e) {
|
|
49078
|
+
return e.IsOnlineMeeting === true && e.OnlineMeetingProvider === "TeamsForBusiness";
|
|
49079
|
+
}
|
|
49080
|
+
function dateTimeTimeZone2(value, zone) {
|
|
49081
|
+
if (/(Z|[+-]\d{2}:?\d{2})$/i.test(value)) {
|
|
49082
|
+
const d = new Date(value);
|
|
49083
|
+
if (!Number.isNaN(d.getTime())) {
|
|
49084
|
+
return { DateTime: d.toISOString().replace(/\.\d{3}Z$/, ""), TimeZone: "UTC" };
|
|
49085
|
+
}
|
|
49086
|
+
}
|
|
49087
|
+
return { DateTime: value, TimeZone: zone };
|
|
49088
|
+
}
|
|
49089
|
+
var naiveMs = (dt) => Date.parse(`${dt.replace(/\.\d+$/, "")}Z`);
|
|
49090
|
+
var fromNaiveMs = (ms) => new Date(ms).toISOString().replace(/\.\d{3}Z$/, "");
|
|
49091
|
+
var trimTime2 = (dt) => dt?.replace(/\.\d+$/, "");
|
|
48811
49092
|
function registerWriteTools(server, client2) {
|
|
48812
49093
|
server.registerTool(
|
|
48813
49094
|
"outlook_send_mail",
|
|
@@ -48967,7 +49248,7 @@ function registerWriteTools(server, client2) {
|
|
|
48967
49248
|
server.registerTool(
|
|
48968
49249
|
"outlook_create_event",
|
|
48969
49250
|
{
|
|
48970
|
-
description: 'Create a calendar event. `timeZone` takes a WINDOWS zone name such as "Eastern Standard Time", not an IANA name. Attendees are emailed an invitation. ' + CONFIRM_FLOW_SENTENCE + " " + CONFIRM_INJECTION_RULE,
|
|
49251
|
+
description: 'Create a calendar event or meeting. A Microsoft Teams meeting is attached by default (`teamsMeeting: false` to skip) and its join link is returned. `timeZone` takes a WINDOWS zone name such as "Eastern Standard Time", not an IANA name. Attendees are emailed an invitation. To pick a time first, use outlook_find_meeting_times. ' + CONFIRM_FLOW_SENTENCE + " " + CONFIRM_INJECTION_RULE,
|
|
48971
49252
|
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
48972
49253
|
inputSchema: external_exports.object({
|
|
48973
49254
|
subject: external_exports.string().describe("Event title"),
|
|
@@ -48976,11 +49257,18 @@ function registerWriteTools(server, client2) {
|
|
|
48976
49257
|
timeZone: external_exports.string().optional().describe("Windows time-zone name. Defaults to the MAILBOX time zone."),
|
|
48977
49258
|
location: external_exports.string().optional().describe("Location display name"),
|
|
48978
49259
|
body: external_exports.string().optional().describe("Event description"),
|
|
48979
|
-
attendees: recipientList,
|
|
49260
|
+
attendees: recipientList.describe("Required attendees' email addresses"),
|
|
49261
|
+
optionalAttendees: recipientList.describe("Optional attendees' email addresses"),
|
|
49262
|
+
teamsMeeting: teamsMeetingParam,
|
|
48980
49263
|
confirmToken: confirmTokenParam
|
|
48981
49264
|
})
|
|
48982
49265
|
},
|
|
48983
|
-
async ({ subject, start, end, timeZone, location, body, attendees, confirmToken }, ctx) => {
|
|
49266
|
+
async ({ subject, start, end, timeZone, location, body, attendees, optionalAttendees, teamsMeeting, confirmToken }, ctx) => {
|
|
49267
|
+
const wantTeams = teamsMeeting !== false;
|
|
49268
|
+
const allAttendees = [
|
|
49269
|
+
...(attendees ?? []).map((a) => attendee(a, "Required")),
|
|
49270
|
+
...(optionalAttendees ?? []).map((a) => attendee(a, "Optional"))
|
|
49271
|
+
];
|
|
48984
49272
|
const tz = timeZone ?? await mailboxTimeZone(client2) ?? "UTC";
|
|
48985
49273
|
const payload = {
|
|
48986
49274
|
Subject: subject,
|
|
@@ -48988,18 +49276,14 @@ function registerWriteTools(server, client2) {
|
|
|
48988
49276
|
End: { DateTime: end, TimeZone: tz },
|
|
48989
49277
|
...location ? { Location: { DisplayName: location } } : {},
|
|
48990
49278
|
...body ? { Body: { ContentType: "Text", Content: body } } : {},
|
|
48991
|
-
...
|
|
48992
|
-
|
|
48993
|
-
EmailAddress: { Address },
|
|
48994
|
-
Type: "Required"
|
|
48995
|
-
}))
|
|
48996
|
-
} : {}
|
|
49279
|
+
...allAttendees.length ? { Attendees: allAttendees } : {},
|
|
49280
|
+
...wantTeams ? TEAMS : {}
|
|
48997
49281
|
};
|
|
48998
49282
|
const gate = await confirmWrite(ctx, {
|
|
48999
49283
|
tool: "outlook_create_event",
|
|
49000
49284
|
action: "calendar.create_event",
|
|
49001
49285
|
message: "Review and confirm this event (attendees are emailed an invitation):",
|
|
49002
|
-
summary: `Create event "${subject}" ${start} to ${end} (${tz})`,
|
|
49286
|
+
summary: `Create ${wantTeams ? "Teams meeting" : "event"} "${subject}" ${start} to ${end} (${tz})`,
|
|
49003
49287
|
// One signed-in mailbox per server process.
|
|
49004
49288
|
account: void 0,
|
|
49005
49289
|
request: { method: "POST", path: "/me/events", body: payload },
|
|
@@ -49011,7 +49295,136 @@ function registerWriteTools(server, client2) {
|
|
|
49011
49295
|
"/me/events",
|
|
49012
49296
|
payload
|
|
49013
49297
|
);
|
|
49014
|
-
|
|
49298
|
+
let joinUrl = joinUrlOf(created);
|
|
49299
|
+
if (wantTeams && !joinUrl && created?.Id) {
|
|
49300
|
+
const after = await client2.get(
|
|
49301
|
+
`/me/events/${encodeURIComponent(created.Id)}?$select=OnlineMeeting`
|
|
49302
|
+
);
|
|
49303
|
+
joinUrl = joinUrlOf(after);
|
|
49304
|
+
}
|
|
49305
|
+
return minifiedResult({
|
|
49306
|
+
created: true,
|
|
49307
|
+
Id: created?.Id,
|
|
49308
|
+
WebLink: created?.WebLink,
|
|
49309
|
+
...joinUrl ? { JoinUrl: joinUrl } : {},
|
|
49310
|
+
...wantTeams && !joinUrl ? { warning: "The event was created, but Outlook returned no Teams join link." } : {}
|
|
49311
|
+
});
|
|
49312
|
+
}
|
|
49313
|
+
);
|
|
49314
|
+
server.registerTool(
|
|
49315
|
+
"outlook_update_event",
|
|
49316
|
+
{
|
|
49317
|
+
description: "Update a meeting you organize: retitle, move, relocate, edit the description, or add/remove attendees. Moving only `start` keeps the meeting's length. A Microsoft Teams meeting is added if the event lacks one (`teamsMeeting: false` to skip). Attendees are sent an updated invitation. Only the organizer can update a meeting \u2014 an attendee's edit would change their own copy alone, so it is refused. The result is verified by re-reading the event. " + CONFIRM_FLOW_SENTENCE + " " + CONFIRM_INJECTION_RULE,
|
|
49318
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true },
|
|
49319
|
+
inputSchema: external_exports.object({
|
|
49320
|
+
id: external_exports.string().min(1).describe("Event Id from outlook_list_events or outlook_get_event"),
|
|
49321
|
+
subject: external_exports.string().optional().describe("New title"),
|
|
49322
|
+
start: external_exports.string().optional().describe("New start, ISO 8601 local time e.g. 2026-09-22T15:00:00"),
|
|
49323
|
+
end: external_exports.string().optional().describe("New end. Omit when moving `start` to keep the current length."),
|
|
49324
|
+
timeZone: external_exports.string().optional().describe("Windows time-zone name for start/end. Defaults to the MAILBOX time zone."),
|
|
49325
|
+
location: external_exports.string().optional().describe("New location display name"),
|
|
49326
|
+
body: external_exports.string().optional().describe("New description (replaces the existing one)"),
|
|
49327
|
+
addAttendees: recipientList.describe("Required attendees to add"),
|
|
49328
|
+
addOptionalAttendees: recipientList.describe("Optional attendees to add"),
|
|
49329
|
+
removeAttendees: recipientList.describe("Attendees to remove"),
|
|
49330
|
+
teamsMeeting: teamsMeetingParam,
|
|
49331
|
+
confirmToken: confirmTokenParam
|
|
49332
|
+
})
|
|
49333
|
+
},
|
|
49334
|
+
async (args, ctx) => {
|
|
49335
|
+
const tz = args.timeZone ?? await mailboxTimeZone(client2) ?? "UTC";
|
|
49336
|
+
const path = `/me/events/${encodeURIComponent(args.id)}`;
|
|
49337
|
+
const readEvent = (zone) => client2.get(`${path}?$select=${EVENT_STATE_SELECT}`, {
|
|
49338
|
+
prefer: `outlook.timezone="${zone}"`
|
|
49339
|
+
});
|
|
49340
|
+
const current = await readEvent(tz);
|
|
49341
|
+
if (current?.IsOrganizer === false) {
|
|
49342
|
+
throw new McpToolError("Only the organizer can update this meeting.", {
|
|
49343
|
+
hint: "You are an attendee. Changing your copy would not reach the organizer or anyone else; ask the organizer, or propose a new time from Outlook."
|
|
49344
|
+
});
|
|
49345
|
+
}
|
|
49346
|
+
const payload = {};
|
|
49347
|
+
if (args.subject !== void 0) payload.Subject = args.subject;
|
|
49348
|
+
if (args.location !== void 0) payload.Location = { DisplayName: args.location };
|
|
49349
|
+
if (args.body !== void 0) payload.Body = { ContentType: "Text", Content: args.body };
|
|
49350
|
+
if (args.start !== void 0 || args.end !== void 0) {
|
|
49351
|
+
const start = args.start !== void 0 ? dateTimeTimeZone2(args.start, tz) : void 0;
|
|
49352
|
+
let end = args.end !== void 0 ? dateTimeTimeZone2(args.end, tz) : void 0;
|
|
49353
|
+
if (start && !end && current?.Start?.DateTime && current?.End?.DateTime) {
|
|
49354
|
+
const length = naiveMs(current.End.DateTime) - naiveMs(current.Start.DateTime);
|
|
49355
|
+
end = { DateTime: fromNaiveMs(naiveMs(start.DateTime) + length), TimeZone: start.TimeZone };
|
|
49356
|
+
}
|
|
49357
|
+
if (args.start !== void 0 && args.end !== void 0 && start?.TimeZone !== end?.TimeZone) {
|
|
49358
|
+
throw new McpToolError("`start` and `end` are in different zones.", {
|
|
49359
|
+
hint: "Give both the same way: both local wall-clock times, or both with Z or an offset."
|
|
49360
|
+
});
|
|
49361
|
+
}
|
|
49362
|
+
const startIn = start ?? (end && end.TimeZone !== tz ? (await readEvent(end.TimeZone))?.Start : current?.Start);
|
|
49363
|
+
if (end && startIn?.DateTime && naiveMs(end.DateTime) <= naiveMs(startIn.DateTime)) {
|
|
49364
|
+
throw new McpToolError("The meeting would end before it starts.", {
|
|
49365
|
+
hint: "`end` must be later than `start`."
|
|
49366
|
+
});
|
|
49367
|
+
}
|
|
49368
|
+
if (start) payload.Start = start;
|
|
49369
|
+
if (end) payload.End = end;
|
|
49370
|
+
}
|
|
49371
|
+
const adding = [
|
|
49372
|
+
...(args.addAttendees ?? []).map((a) => attendee(a, "Required")),
|
|
49373
|
+
...(args.addOptionalAttendees ?? []).map((a) => attendee(a, "Optional"))
|
|
49374
|
+
];
|
|
49375
|
+
const removing = new Set((args.removeAttendees ?? []).map((a) => a.toLowerCase()));
|
|
49376
|
+
if (adding.length || removing.size) {
|
|
49377
|
+
const next = (current?.Attendees ?? []).filter((a) => !removing.has(lower(a) ?? "")).map(({ Type, EmailAddress }) => ({ Type, EmailAddress }));
|
|
49378
|
+
for (const a of adding) {
|
|
49379
|
+
if (!next.some((n) => lower(n) === lower(a))) next.push(a);
|
|
49380
|
+
}
|
|
49381
|
+
payload.Attendees = next;
|
|
49382
|
+
}
|
|
49383
|
+
const addTeams = args.teamsMeeting !== false && !(current && isTeams(current));
|
|
49384
|
+
if (addTeams) Object.assign(payload, TEAMS);
|
|
49385
|
+
if (Object.keys(payload).length === 0) {
|
|
49386
|
+
throw new McpToolError("Nothing to update.", {
|
|
49387
|
+
hint: "Pass at least one of subject, start, end, location, body, or attendee changes."
|
|
49388
|
+
});
|
|
49389
|
+
}
|
|
49390
|
+
const gate = await confirmWrite(ctx, {
|
|
49391
|
+
tool: "outlook_update_event",
|
|
49392
|
+
action: "calendar.update_event",
|
|
49393
|
+
message: "Review and confirm this change (attendees are sent an updated invitation):",
|
|
49394
|
+
summary: `Update "${current?.Subject ?? args.id}": ${Object.keys(payload).join(", ")}`,
|
|
49395
|
+
// One signed-in mailbox per server process.
|
|
49396
|
+
account: void 0,
|
|
49397
|
+
target: args.id,
|
|
49398
|
+
request: { method: "PATCH", path, body: payload },
|
|
49399
|
+
confirmToken: args.confirmToken
|
|
49400
|
+
});
|
|
49401
|
+
if (gate) return gate;
|
|
49402
|
+
await client2.write("PATCH", path, payload);
|
|
49403
|
+
const writtenZone = (payload.Start ?? payload.End)?.TimeZone ?? tz;
|
|
49404
|
+
const after = await readEvent(writtenZone);
|
|
49405
|
+
const unchanged = [];
|
|
49406
|
+
if (payload.Subject !== void 0 && after?.Subject !== payload.Subject) unchanged.push("Subject");
|
|
49407
|
+
for (const key of ["Start", "End"]) {
|
|
49408
|
+
const want = payload[key];
|
|
49409
|
+
if (want && trimTime2(after?.[key]?.DateTime) !== trimTime2(want.DateTime)) unchanged.push(key);
|
|
49410
|
+
}
|
|
49411
|
+
if (payload.Location !== void 0 && after?.Location?.DisplayName !== args.location) {
|
|
49412
|
+
unchanged.push("Location");
|
|
49413
|
+
}
|
|
49414
|
+
if (payload.Attendees !== void 0) {
|
|
49415
|
+
const want = payload.Attendees.map(lower).sort().join();
|
|
49416
|
+
const got = (after?.Attendees ?? []).map(lower).sort().join();
|
|
49417
|
+
if (want !== got) unchanged.push("Attendees");
|
|
49418
|
+
}
|
|
49419
|
+
const joinUrl = joinUrlOf(after);
|
|
49420
|
+
if (addTeams && !joinUrl) unchanged.push("Teams meeting");
|
|
49421
|
+
return minifiedResult({
|
|
49422
|
+
updated: unchanged.length === 0,
|
|
49423
|
+
Id: after?.Id ?? args.id,
|
|
49424
|
+
changed: Object.keys(payload),
|
|
49425
|
+
...joinUrl ? { JoinUrl: joinUrl } : {},
|
|
49426
|
+
...unchanged.length ? { warning: `Outlook accepted the update but these did not change: ${unchanged.join(", ")}.` } : {}
|
|
49427
|
+
});
|
|
49015
49428
|
}
|
|
49016
49429
|
);
|
|
49017
49430
|
}
|
package/dist/client.js
CHANGED
|
@@ -228,6 +228,19 @@ export class OutlookClient {
|
|
|
228
228
|
}
|
|
229
229
|
return this.get(target.pathname.slice(prefix.length) + target.search, opts);
|
|
230
230
|
}
|
|
231
|
+
/**
|
|
232
|
+
* POST a read-only query — `findmeetingtimes`, `getschedule` — that changes
|
|
233
|
+
* nothing in the mailbox. Kept apart from `write` so a reader can tell the
|
|
234
|
+
* two apart at the call site, and because these take `Prefer` (notably
|
|
235
|
+
* `outlook.timezone`, without which every returned slot is UTC).
|
|
236
|
+
*/
|
|
237
|
+
async post(path, body, opts = {}) {
|
|
238
|
+
this.#requireConfigured();
|
|
239
|
+
return this.#client().fetchJson('POST', path, {
|
|
240
|
+
body,
|
|
241
|
+
headers: opts.prefer ? { Prefer: opts.prefer } : undefined,
|
|
242
|
+
});
|
|
243
|
+
}
|
|
231
244
|
/**
|
|
232
245
|
* The single write path. Everything mutating goes through here so auth and
|
|
233
246
|
* error shaping stay in one place.
|
package/dist/tools/calendar.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import { minifiedResult, resolveView, viewParam } from '@chrischall/mcp-utils';
|
|
2
|
+
import { McpToolError, minifiedResult, resolveView, viewParam } from '@chrischall/mcp-utils';
|
|
3
3
|
import { mailboxUntrusted, UNTRUSTED_DESCRIPTION_SUFFIX } from './_untrusted.js';
|
|
4
4
|
import { compactEvent, fullEvent, projectCollection, } from '../view.js';
|
|
5
5
|
import { fetchPage, nextLinkParam, plainCollection } from './_paging.js';
|
|
6
6
|
import { VIEWS } from './mail.js';
|
|
7
7
|
import { mailboxTimeZone } from '../timezone.js';
|
|
8
|
-
const EVENT_SELECT = 'Id,Subject,Start,End,Location,Organizer,IsAllDay,IsCancelled,ShowAs,OnlineMeetingUrl,BodyPreview';
|
|
8
|
+
const EVENT_SELECT = 'Id,Subject,Start,End,Location,Organizer,IsAllDay,IsCancelled,ShowAs,OnlineMeetingUrl,OnlineMeeting,BodyPreview';
|
|
9
9
|
function qs(params) {
|
|
10
10
|
const parts = [];
|
|
11
11
|
for (const [k, v] of Object.entries(params)) {
|
|
@@ -15,6 +15,37 @@ function qs(params) {
|
|
|
15
15
|
}
|
|
16
16
|
return parts.length ? `?${parts.join('&')}` : '';
|
|
17
17
|
}
|
|
18
|
+
/**
|
|
19
|
+
* An Outlook `DateTimeTimeZone` for a caller-supplied time.
|
|
20
|
+
*
|
|
21
|
+
* A wall-clock time ("2026-10-12T09:00:00") is read in `zone`, matching
|
|
22
|
+
* outlook_create_event. One carrying `Z` or an offset already names its
|
|
23
|
+
* instant, so it is converted to UTC rather than having its offset silently
|
|
24
|
+
* ignored and re-read in the mailbox zone.
|
|
25
|
+
*/
|
|
26
|
+
function dateTimeTimeZone(value, zone) {
|
|
27
|
+
if (/(Z|[+-]\d{2}:?\d{2})$/i.test(value)) {
|
|
28
|
+
const d = new Date(value);
|
|
29
|
+
if (!Number.isNaN(d.getTime())) {
|
|
30
|
+
return { DateTime: d.toISOString().replace(/\.\d{3}Z$/, ''), TimeZone: 'UTC' };
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return { DateTime: value, TimeZone: zone };
|
|
34
|
+
}
|
|
35
|
+
function assertWindow(start, end) {
|
|
36
|
+
const a = Date.parse(start);
|
|
37
|
+
const b = Date.parse(end);
|
|
38
|
+
if (!Number.isNaN(a) && !Number.isNaN(b) && b <= a) {
|
|
39
|
+
throw new McpToolError(`The window ends (${end}) before it starts (${start}).`, {
|
|
40
|
+
hint: '`end` must be later than `start`.',
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/** Outlook pads times to 7 fractional digits; that is noise to a reader. */
|
|
45
|
+
function trimTime(dt) {
|
|
46
|
+
return dt?.replace(/\.0+$/, '');
|
|
47
|
+
}
|
|
48
|
+
const attendeeList = (what) => z.array(z.string().min(3)).describe(`Email addresses of ${what}`);
|
|
18
49
|
export function registerCalendarTools(server, client) {
|
|
19
50
|
server.registerTool('outlook_list_events', {
|
|
20
51
|
description: 'List calendar events in a date window. This uses the calendar VIEW, which expands recurring series into their individual occurrences — the right tool for "what is on my schedule". Times are returned in `timeZone` when given.' +
|
|
@@ -78,4 +109,135 @@ export function registerCalendarTools(server, client) {
|
|
|
78
109
|
return minifiedResult(data);
|
|
79
110
|
return minifiedResult(plainCollection(data));
|
|
80
111
|
});
|
|
112
|
+
server.registerTool('outlook_find_meeting_times', {
|
|
113
|
+
description: "Find times when people can meet — Outlook's Scheduling Assistant. Give the attendees, a search window and a duration; Outlook checks everyone's free/busy (and the signed-in user's, as organizer) and returns ranked candidate slots with each attendee's availability. By default only working hours are searched. Use this to answer \"when can X and Y meet?\"; then book with outlook_create_event. `start`/`end` are local wall-clock times in `timeZone` (default: the mailbox zone), and returned slots are in that zone too." +
|
|
114
|
+
' ' + UNTRUSTED_DESCRIPTION_SUFFIX,
|
|
115
|
+
annotations: { readOnlyHint: true },
|
|
116
|
+
inputSchema: z.object({
|
|
117
|
+
view: viewParam(VIEWS),
|
|
118
|
+
attendees: attendeeList('required attendees').min(1),
|
|
119
|
+
optionalAttendees: attendeeList('optional attendees').optional(),
|
|
120
|
+
start: z.string().min(1).describe('Earliest the meeting may start, e.g. 2026-10-12T09:00:00'),
|
|
121
|
+
end: z.string().min(1).describe('Latest the meeting may end, e.g. 2026-10-16T17:00:00'),
|
|
122
|
+
durationMinutes: z.number().int().min(5).max(1440).describe('Meeting length in minutes'),
|
|
123
|
+
timeZone: z
|
|
124
|
+
.string()
|
|
125
|
+
.optional()
|
|
126
|
+
.describe('Windows time-zone name, e.g. "Eastern Standard Time". Defaults to the MAILBOX time zone.'),
|
|
127
|
+
maxCandidates: z.number().int().min(1).max(50).optional().describe('Max suggestions (default 10)'),
|
|
128
|
+
workingHoursOnly: z
|
|
129
|
+
.boolean()
|
|
130
|
+
.optional()
|
|
131
|
+
.describe("Only suggest times inside working hours (default true). false searches around the clock."),
|
|
132
|
+
minimumAttendeePercentage: z
|
|
133
|
+
.number()
|
|
134
|
+
.min(0)
|
|
135
|
+
.max(100)
|
|
136
|
+
.optional()
|
|
137
|
+
.describe('Lowest confidence (0-100) a slot needs to be suggested. Lower it to see slots where not everyone is free.'),
|
|
138
|
+
isOrganizerOptional: z
|
|
139
|
+
.boolean()
|
|
140
|
+
.optional()
|
|
141
|
+
.describe("true when the signed-in user need not attend, so their own calendar is ignored"),
|
|
142
|
+
}),
|
|
143
|
+
}, async (args) => {
|
|
144
|
+
assertWindow(args.start, args.end);
|
|
145
|
+
const zone = args.timeZone ?? (await mailboxTimeZone(client)) ?? 'UTC';
|
|
146
|
+
const body = {
|
|
147
|
+
Attendees: [
|
|
148
|
+
...args.attendees.map((Address) => ({ Type: 'Required', EmailAddress: { Address } })),
|
|
149
|
+
...(args.optionalAttendees ?? []).map((Address) => ({
|
|
150
|
+
Type: 'Optional',
|
|
151
|
+
EmailAddress: { Address },
|
|
152
|
+
})),
|
|
153
|
+
],
|
|
154
|
+
TimeConstraint: {
|
|
155
|
+
ActivityDomain: args.workingHoursOnly === false ? 'Unrestricted' : 'Work',
|
|
156
|
+
Timeslots: [
|
|
157
|
+
{ Start: dateTimeTimeZone(args.start, zone), End: dateTimeTimeZone(args.end, zone) },
|
|
158
|
+
],
|
|
159
|
+
},
|
|
160
|
+
MeetingDuration: `PT${args.durationMinutes}M`,
|
|
161
|
+
MaxCandidates: args.maxCandidates ?? 10,
|
|
162
|
+
ReturnSuggestionReasons: true,
|
|
163
|
+
...(args.minimumAttendeePercentage !== undefined
|
|
164
|
+
? { MinimumAttendeePercentage: args.minimumAttendeePercentage }
|
|
165
|
+
: {}),
|
|
166
|
+
...(args.isOrganizerOptional !== undefined
|
|
167
|
+
? { IsOrganizerOptional: args.isOrganizerOptional }
|
|
168
|
+
: {}),
|
|
169
|
+
};
|
|
170
|
+
const data = await client.post('/me/findmeetingtimes', body, { prefer: `outlook.timezone="${zone}"` });
|
|
171
|
+
if (resolveView(args.view, VIEWS) === 'raw')
|
|
172
|
+
return mailboxUntrusted(data);
|
|
173
|
+
const suggestions = (data?.MeetingTimeSuggestions ?? []).map((s) => ({
|
|
174
|
+
Start: trimTime(s.MeetingTimeSlot?.Start?.DateTime),
|
|
175
|
+
End: trimTime(s.MeetingTimeSlot?.End?.DateTime),
|
|
176
|
+
Confidence: s.Confidence,
|
|
177
|
+
Organizer: s.OrganizerAvailability,
|
|
178
|
+
Attendees: Object.fromEntries((s.AttendeeAvailability ?? []).map((a) => [
|
|
179
|
+
a.Attendee?.EmailAddress?.Address ?? a.Attendee?.EmailAddress?.Name ?? '?',
|
|
180
|
+
a.Availability,
|
|
181
|
+
])),
|
|
182
|
+
Reason: s.SuggestionReason,
|
|
183
|
+
}));
|
|
184
|
+
return mailboxUntrusted({
|
|
185
|
+
timeZone: zone,
|
|
186
|
+
count: suggestions.length,
|
|
187
|
+
suggestions,
|
|
188
|
+
// Outlook explains an empty result ("AttendeesUnavailable", ...); an
|
|
189
|
+
// empty list without it reads as "nobody is ever free".
|
|
190
|
+
...(data?.EmptySuggestionsReason
|
|
191
|
+
? { emptySuggestionsReason: data.EmptySuggestionsReason }
|
|
192
|
+
: {}),
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
server.registerTool('outlook_get_schedule', {
|
|
196
|
+
description: "Get people's free/busy blocks for a time window — the grid view of Outlook's Scheduling Assistant. Returns each person's busy, tentative and out-of-office items (with subject and location where their calendar shares them). Use outlook_find_meeting_times to have Outlook pick slots instead. `start`/`end` are local wall-clock times in `timeZone` (default: the mailbox zone)." +
|
|
197
|
+
' ' + UNTRUSTED_DESCRIPTION_SUFFIX,
|
|
198
|
+
annotations: { readOnlyHint: true },
|
|
199
|
+
inputSchema: z.object({
|
|
200
|
+
view: viewParam(VIEWS),
|
|
201
|
+
people: attendeeList('people (or rooms) to look up').min(1).max(100),
|
|
202
|
+
start: z.string().min(1).describe('Window start, e.g. 2026-10-12T09:00:00'),
|
|
203
|
+
end: z.string().min(1).describe('Window end, e.g. 2026-10-12T17:00:00'),
|
|
204
|
+
timeZone: z
|
|
205
|
+
.string()
|
|
206
|
+
.optional()
|
|
207
|
+
.describe('Windows time-zone name, e.g. "Eastern Standard Time". Defaults to the MAILBOX time zone.'),
|
|
208
|
+
intervalMinutes: z
|
|
209
|
+
.number()
|
|
210
|
+
.int()
|
|
211
|
+
.min(5)
|
|
212
|
+
.max(1440)
|
|
213
|
+
.optional()
|
|
214
|
+
.describe('Slot size for the raw view\'s AvailabilityView string (default 30)'),
|
|
215
|
+
}),
|
|
216
|
+
}, async ({ view, people, start, end, timeZone, intervalMinutes }) => {
|
|
217
|
+
assertWindow(start, end);
|
|
218
|
+
const zone = timeZone ?? (await mailboxTimeZone(client)) ?? 'UTC';
|
|
219
|
+
const data = await client.post('/me/calendar/getschedule', {
|
|
220
|
+
Schedules: people,
|
|
221
|
+
StartTime: dateTimeTimeZone(start, zone),
|
|
222
|
+
EndTime: dateTimeTimeZone(end, zone),
|
|
223
|
+
AvailabilityViewInterval: intervalMinutes ?? 30,
|
|
224
|
+
}, { prefer: `outlook.timezone="${zone}"` });
|
|
225
|
+
if (resolveView(view, VIEWS) === 'raw')
|
|
226
|
+
return mailboxUntrusted(data);
|
|
227
|
+
const schedules = (data?.value ?? []).map((p) =>
|
|
228
|
+
// An unresolvable address must not read as "free all day".
|
|
229
|
+
p.Error !== undefined && p.Error !== null
|
|
230
|
+
? { Who: p.ScheduleId, Error: p.Error }
|
|
231
|
+
: {
|
|
232
|
+
Who: p.ScheduleId,
|
|
233
|
+
Busy: (p.ScheduleItems ?? []).map((i) => ({
|
|
234
|
+
Start: trimTime(i.Start?.DateTime),
|
|
235
|
+
End: trimTime(i.End?.DateTime),
|
|
236
|
+
Status: i.Status,
|
|
237
|
+
...(i.Subject ? { Subject: i.Subject } : {}),
|
|
238
|
+
...(i.Location ? { Location: i.Location } : {}),
|
|
239
|
+
})),
|
|
240
|
+
});
|
|
241
|
+
return mailboxUntrusted({ timeZone: zone, schedules });
|
|
242
|
+
});
|
|
81
243
|
}
|
package/dist/tools/writes.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import { CONFIRM_FLOW_SENTENCE, CONFIRM_INJECTION_RULE, confirmTokenParam, confirmWrite, minifiedResult, } from '@chrischall/mcp-utils';
|
|
2
|
+
import { CONFIRM_FLOW_SENTENCE, CONFIRM_INJECTION_RULE, confirmTokenParam, confirmWrite, McpToolError, minifiedResult, } from '@chrischall/mcp-utils';
|
|
3
3
|
import { mailboxTimeZone } from '../timezone.js';
|
|
4
4
|
const recipientList = z
|
|
5
5
|
.array(z.string().min(3))
|
|
@@ -18,6 +18,44 @@ function messageBody(subject, body, html) {
|
|
|
18
18
|
Body: { ContentType: html === true ? 'HTML' : 'Text', Content: body },
|
|
19
19
|
};
|
|
20
20
|
}
|
|
21
|
+
/**
|
|
22
|
+
* What makes an event a Teams meeting. Field names verified on live events
|
|
23
|
+
* 2026-10-08; the join link then appears at `OnlineMeeting.JoinUrl`.
|
|
24
|
+
*/
|
|
25
|
+
const TEAMS = { IsOnlineMeeting: true, OnlineMeetingProvider: 'TeamsForBusiness' };
|
|
26
|
+
const teamsMeetingParam = z
|
|
27
|
+
.boolean()
|
|
28
|
+
.optional()
|
|
29
|
+
.describe('Attach a Microsoft Teams meeting (default true). false leaves it off.');
|
|
30
|
+
const EVENT_STATE_SELECT = 'Id,Subject,IsOrganizer,Start,End,Location,Attendees,IsOnlineMeeting,OnlineMeetingProvider,OnlineMeeting';
|
|
31
|
+
function attendee(Address, Type) {
|
|
32
|
+
return { Type, EmailAddress: { Address } };
|
|
33
|
+
}
|
|
34
|
+
const lower = (a) => a.EmailAddress?.Address?.toLowerCase();
|
|
35
|
+
function joinUrlOf(e) {
|
|
36
|
+
return e?.OnlineMeeting?.JoinUrl || undefined;
|
|
37
|
+
}
|
|
38
|
+
function isTeams(e) {
|
|
39
|
+
return e.IsOnlineMeeting === true && e.OnlineMeetingProvider === 'TeamsForBusiness';
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* A `DateTimeTimeZone` for a caller-supplied time: wall-clock is read in
|
|
43
|
+
* `zone`; a value with `Z` or an offset already names its instant and goes to
|
|
44
|
+
* UTC rather than having the offset ignored.
|
|
45
|
+
*/
|
|
46
|
+
function dateTimeTimeZone(value, zone) {
|
|
47
|
+
if (/(Z|[+-]\d{2}:?\d{2})$/i.test(value)) {
|
|
48
|
+
const d = new Date(value);
|
|
49
|
+
if (!Number.isNaN(d.getTime())) {
|
|
50
|
+
return { DateTime: d.toISOString().replace(/\.\d{3}Z$/, ''), TimeZone: 'UTC' };
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return { DateTime: value, TimeZone: zone };
|
|
54
|
+
}
|
|
55
|
+
/** Wall-clock arithmetic: both sides are naive times in one zone. */
|
|
56
|
+
const naiveMs = (dt) => Date.parse(`${dt.replace(/\.\d+$/, '')}Z`);
|
|
57
|
+
const fromNaiveMs = (ms) => new Date(ms).toISOString().replace(/\.\d{3}Z$/, '');
|
|
58
|
+
const trimTime = (dt) => dt?.replace(/\.\d+$/, '');
|
|
21
59
|
export function registerWriteTools(server, client) {
|
|
22
60
|
server.registerTool('outlook_send_mail', {
|
|
23
61
|
description: 'Send an email from the signed-in mailbox.' +
|
|
@@ -172,7 +210,7 @@ export function registerWriteTools(server, client) {
|
|
|
172
210
|
});
|
|
173
211
|
});
|
|
174
212
|
server.registerTool('outlook_create_event', {
|
|
175
|
-
description: 'Create a calendar event. `timeZone` takes a WINDOWS zone name such as "Eastern Standard Time", not an IANA name. Attendees are emailed an invitation.' +
|
|
213
|
+
description: 'Create a calendar event or meeting. A Microsoft Teams meeting is attached by default (`teamsMeeting: false` to skip) and its join link is returned. `timeZone` takes a WINDOWS zone name such as "Eastern Standard Time", not an IANA name. Attendees are emailed an invitation. To pick a time first, use outlook_find_meeting_times.' +
|
|
176
214
|
' ' +
|
|
177
215
|
CONFIRM_FLOW_SENTENCE +
|
|
178
216
|
' ' +
|
|
@@ -188,10 +226,17 @@ export function registerWriteTools(server, client) {
|
|
|
188
226
|
.describe('Windows time-zone name. Defaults to the MAILBOX time zone.'),
|
|
189
227
|
location: z.string().optional().describe('Location display name'),
|
|
190
228
|
body: z.string().optional().describe('Event description'),
|
|
191
|
-
attendees: recipientList,
|
|
229
|
+
attendees: recipientList.describe('Required attendees\' email addresses'),
|
|
230
|
+
optionalAttendees: recipientList.describe('Optional attendees\' email addresses'),
|
|
231
|
+
teamsMeeting: teamsMeetingParam,
|
|
192
232
|
confirmToken: confirmTokenParam,
|
|
193
233
|
}),
|
|
194
|
-
}, async ({ subject, start, end, timeZone, location, body, attendees, confirmToken }, ctx) => {
|
|
234
|
+
}, async ({ subject, start, end, timeZone, location, body, attendees, optionalAttendees, teamsMeeting, confirmToken }, ctx) => {
|
|
235
|
+
const wantTeams = teamsMeeting !== false;
|
|
236
|
+
const allAttendees = [
|
|
237
|
+
...(attendees ?? []).map((a) => attendee(a, 'Required')),
|
|
238
|
+
...(optionalAttendees ?? []).map((a) => attendee(a, 'Optional')),
|
|
239
|
+
];
|
|
195
240
|
// Falls back to UTC only when the mailbox itself declares no zone.
|
|
196
241
|
const tz = timeZone ?? (await mailboxTimeZone(client)) ?? 'UTC';
|
|
197
242
|
const payload = {
|
|
@@ -200,20 +245,14 @@ export function registerWriteTools(server, client) {
|
|
|
200
245
|
End: { DateTime: end, TimeZone: tz },
|
|
201
246
|
...(location ? { Location: { DisplayName: location } } : {}),
|
|
202
247
|
...(body ? { Body: { ContentType: 'Text', Content: body } } : {}),
|
|
203
|
-
...(
|
|
204
|
-
|
|
205
|
-
Attendees: attendees.map((Address) => ({
|
|
206
|
-
EmailAddress: { Address },
|
|
207
|
-
Type: 'Required',
|
|
208
|
-
})),
|
|
209
|
-
}
|
|
210
|
-
: {}),
|
|
248
|
+
...(allAttendees.length ? { Attendees: allAttendees } : {}),
|
|
249
|
+
...(wantTeams ? TEAMS : {}),
|
|
211
250
|
};
|
|
212
251
|
const gate = await confirmWrite(ctx, {
|
|
213
252
|
tool: 'outlook_create_event',
|
|
214
253
|
action: 'calendar.create_event',
|
|
215
254
|
message: 'Review and confirm this event (attendees are emailed an invitation):',
|
|
216
|
-
summary: `Create event "${subject}" ${start} to ${end} (${tz})`,
|
|
255
|
+
summary: `Create ${wantTeams ? 'Teams meeting' : 'event'} "${subject}" ${start} to ${end} (${tz})`,
|
|
217
256
|
// One signed-in mailbox per server process.
|
|
218
257
|
account: undefined,
|
|
219
258
|
request: { method: 'POST', path: '/me/events', body: payload },
|
|
@@ -222,6 +261,175 @@ export function registerWriteTools(server, client) {
|
|
|
222
261
|
if (gate)
|
|
223
262
|
return gate;
|
|
224
263
|
const created = await client.write('POST', '/me/events', payload);
|
|
225
|
-
|
|
264
|
+
let joinUrl = joinUrlOf(created);
|
|
265
|
+
// A 201 is not proof the Teams meeting was provisioned — the link is.
|
|
266
|
+
// Re-read once in case it was filled in after the create returned.
|
|
267
|
+
if (wantTeams && !joinUrl && created?.Id) {
|
|
268
|
+
const after = await client.get(`/me/events/${encodeURIComponent(created.Id)}?$select=OnlineMeeting`);
|
|
269
|
+
joinUrl = joinUrlOf(after);
|
|
270
|
+
}
|
|
271
|
+
return minifiedResult({
|
|
272
|
+
created: true,
|
|
273
|
+
Id: created?.Id,
|
|
274
|
+
WebLink: created?.WebLink,
|
|
275
|
+
...(joinUrl ? { JoinUrl: joinUrl } : {}),
|
|
276
|
+
...(wantTeams && !joinUrl
|
|
277
|
+
? { warning: 'The event was created, but Outlook returned no Teams join link.' }
|
|
278
|
+
: {}),
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
server.registerTool('outlook_update_event', {
|
|
282
|
+
description: 'Update a meeting you organize: retitle, move, relocate, edit the description, or add/remove attendees. Moving only `start` keeps the meeting\'s length. A Microsoft Teams meeting is added if the event lacks one (`teamsMeeting: false` to skip). Attendees are sent an updated invitation. Only the organizer can update a meeting — an attendee\'s edit would change their own copy alone, so it is refused. The result is verified by re-reading the event.' +
|
|
283
|
+
' ' +
|
|
284
|
+
CONFIRM_FLOW_SENTENCE +
|
|
285
|
+
' ' +
|
|
286
|
+
CONFIRM_INJECTION_RULE,
|
|
287
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true },
|
|
288
|
+
inputSchema: z.object({
|
|
289
|
+
id: z.string().min(1).describe('Event Id from outlook_list_events or outlook_get_event'),
|
|
290
|
+
subject: z.string().optional().describe('New title'),
|
|
291
|
+
start: z.string().optional().describe('New start, ISO 8601 local time e.g. 2026-09-22T15:00:00'),
|
|
292
|
+
end: z
|
|
293
|
+
.string()
|
|
294
|
+
.optional()
|
|
295
|
+
.describe('New end. Omit when moving `start` to keep the current length.'),
|
|
296
|
+
timeZone: z
|
|
297
|
+
.string()
|
|
298
|
+
.optional()
|
|
299
|
+
.describe('Windows time-zone name for start/end. Defaults to the MAILBOX time zone.'),
|
|
300
|
+
location: z.string().optional().describe('New location display name'),
|
|
301
|
+
body: z.string().optional().describe('New description (replaces the existing one)'),
|
|
302
|
+
addAttendees: recipientList.describe('Required attendees to add'),
|
|
303
|
+
addOptionalAttendees: recipientList.describe('Optional attendees to add'),
|
|
304
|
+
removeAttendees: recipientList.describe('Attendees to remove'),
|
|
305
|
+
teamsMeeting: teamsMeetingParam,
|
|
306
|
+
confirmToken: confirmTokenParam,
|
|
307
|
+
}),
|
|
308
|
+
}, async (args, ctx) => {
|
|
309
|
+
const tz = args.timeZone ?? (await mailboxTimeZone(client)) ?? 'UTC';
|
|
310
|
+
const path = `/me/events/${encodeURIComponent(args.id)}`;
|
|
311
|
+
const readEvent = (zone) => client.get(`${path}?$select=${EVENT_STATE_SELECT}`, {
|
|
312
|
+
prefer: `outlook.timezone="${zone}"`,
|
|
313
|
+
});
|
|
314
|
+
const current = await readEvent(tz);
|
|
315
|
+
if (current?.IsOrganizer === false) {
|
|
316
|
+
throw new McpToolError('Only the organizer can update this meeting.', {
|
|
317
|
+
hint: "You are an attendee. Changing your copy would not reach the organizer or anyone else; ask the organizer, or propose a new time from Outlook.",
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
const payload = {};
|
|
321
|
+
if (args.subject !== undefined)
|
|
322
|
+
payload.Subject = args.subject;
|
|
323
|
+
if (args.location !== undefined)
|
|
324
|
+
payload.Location = { DisplayName: args.location };
|
|
325
|
+
if (args.body !== undefined)
|
|
326
|
+
payload.Body = { ContentType: 'Text', Content: args.body };
|
|
327
|
+
if (args.start !== undefined || args.end !== undefined) {
|
|
328
|
+
const start = args.start !== undefined ? dateTimeTimeZone(args.start, tz) : undefined;
|
|
329
|
+
let end = args.end !== undefined ? dateTimeTimeZone(args.end, tz) : undefined;
|
|
330
|
+
// Moving the start alone keeps the length. The current times were read
|
|
331
|
+
// in `tz`, so convert the duration onto whatever zone the new start uses.
|
|
332
|
+
if (start && !end && current?.Start?.DateTime && current?.End?.DateTime) {
|
|
333
|
+
const length = naiveMs(current.End.DateTime) - naiveMs(current.Start.DateTime);
|
|
334
|
+
end = { DateTime: fromNaiveMs(naiveMs(start.DateTime) + length), TimeZone: start.TimeZone };
|
|
335
|
+
}
|
|
336
|
+
// A wall-clock time and one with an offset cannot be ordered without
|
|
337
|
+
// the Windows zone's rules, which we do not have. Refuse rather than
|
|
338
|
+
// skip the order check and write a meeting that ends before it starts.
|
|
339
|
+
if (args.start !== undefined && args.end !== undefined && start?.TimeZone !== end?.TimeZone) {
|
|
340
|
+
throw new McpToolError('`start` and `end` are in different zones.', {
|
|
341
|
+
hint: 'Give both the same way: both local wall-clock times, or both with Z or an offset.',
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
// Order check against the start the meeting will have. When only `end`
|
|
345
|
+
// moves that is the current start, read in the end's zone.
|
|
346
|
+
const startIn = start ??
|
|
347
|
+
(end && end.TimeZone !== tz
|
|
348
|
+
? (await readEvent(end.TimeZone))?.Start
|
|
349
|
+
: current?.Start);
|
|
350
|
+
if (end &&
|
|
351
|
+
startIn?.DateTime &&
|
|
352
|
+
naiveMs(end.DateTime) <= naiveMs(startIn.DateTime)) {
|
|
353
|
+
throw new McpToolError('The meeting would end before it starts.', {
|
|
354
|
+
hint: '`end` must be later than `start`.',
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
if (start)
|
|
358
|
+
payload.Start = start;
|
|
359
|
+
if (end)
|
|
360
|
+
payload.End = end;
|
|
361
|
+
}
|
|
362
|
+
const adding = [
|
|
363
|
+
...(args.addAttendees ?? []).map((a) => attendee(a, 'Required')),
|
|
364
|
+
...(args.addOptionalAttendees ?? []).map((a) => attendee(a, 'Optional')),
|
|
365
|
+
];
|
|
366
|
+
const removing = new Set((args.removeAttendees ?? []).map((a) => a.toLowerCase()));
|
|
367
|
+
if (adding.length || removing.size) {
|
|
368
|
+
// PATCH replaces the whole list, so build it from the current one.
|
|
369
|
+
const next = (current?.Attendees ?? [])
|
|
370
|
+
.filter((a) => !removing.has(lower(a) ?? ''))
|
|
371
|
+
.map(({ Type, EmailAddress }) => ({ Type, EmailAddress }));
|
|
372
|
+
for (const a of adding) {
|
|
373
|
+
if (!next.some((n) => lower(n) === lower(a)))
|
|
374
|
+
next.push(a);
|
|
375
|
+
}
|
|
376
|
+
payload.Attendees = next;
|
|
377
|
+
}
|
|
378
|
+
const addTeams = args.teamsMeeting !== false && !(current && isTeams(current));
|
|
379
|
+
if (addTeams)
|
|
380
|
+
Object.assign(payload, TEAMS);
|
|
381
|
+
if (Object.keys(payload).length === 0) {
|
|
382
|
+
throw new McpToolError('Nothing to update.', {
|
|
383
|
+
hint: 'Pass at least one of subject, start, end, location, body, or attendee changes.',
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
const gate = await confirmWrite(ctx, {
|
|
387
|
+
tool: 'outlook_update_event',
|
|
388
|
+
action: 'calendar.update_event',
|
|
389
|
+
message: 'Review and confirm this change (attendees are sent an updated invitation):',
|
|
390
|
+
summary: `Update "${current?.Subject ?? args.id}": ${Object.keys(payload).join(', ')}`,
|
|
391
|
+
// One signed-in mailbox per server process.
|
|
392
|
+
account: undefined,
|
|
393
|
+
target: args.id,
|
|
394
|
+
request: { method: 'PATCH', path, body: payload },
|
|
395
|
+
confirmToken: args.confirmToken,
|
|
396
|
+
});
|
|
397
|
+
if (gate)
|
|
398
|
+
return gate;
|
|
399
|
+
await client.write('PATCH', path, payload);
|
|
400
|
+
// Re-read rather than trust the status, in the zone the write used so
|
|
401
|
+
// the times compare like for like.
|
|
402
|
+
// Start and End share a zone (enforced above), but either may be alone.
|
|
403
|
+
const writtenZone = (payload.Start ?? payload.End)?.TimeZone ?? tz;
|
|
404
|
+
const after = await readEvent(writtenZone);
|
|
405
|
+
const unchanged = [];
|
|
406
|
+
if (payload.Subject !== undefined && after?.Subject !== payload.Subject)
|
|
407
|
+
unchanged.push('Subject');
|
|
408
|
+
for (const key of ['Start', 'End']) {
|
|
409
|
+
const want = payload[key];
|
|
410
|
+
if (want && trimTime(after?.[key]?.DateTime) !== trimTime(want.DateTime))
|
|
411
|
+
unchanged.push(key);
|
|
412
|
+
}
|
|
413
|
+
if (payload.Location !== undefined && after?.Location?.DisplayName !== args.location) {
|
|
414
|
+
unchanged.push('Location');
|
|
415
|
+
}
|
|
416
|
+
if (payload.Attendees !== undefined) {
|
|
417
|
+
const want = payload.Attendees.map(lower).sort().join();
|
|
418
|
+
const got = (after?.Attendees ?? []).map(lower).sort().join();
|
|
419
|
+
if (want !== got)
|
|
420
|
+
unchanged.push('Attendees');
|
|
421
|
+
}
|
|
422
|
+
const joinUrl = joinUrlOf(after);
|
|
423
|
+
if (addTeams && !joinUrl)
|
|
424
|
+
unchanged.push('Teams meeting');
|
|
425
|
+
return minifiedResult({
|
|
426
|
+
updated: unchanged.length === 0,
|
|
427
|
+
Id: after?.Id ?? args.id,
|
|
428
|
+
changed: Object.keys(payload),
|
|
429
|
+
...(joinUrl ? { JoinUrl: joinUrl } : {}),
|
|
430
|
+
...(unchanged.length
|
|
431
|
+
? { warning: `Outlook accepted the update but these did not change: ${unchanged.join(', ')}.` }
|
|
432
|
+
: {}),
|
|
433
|
+
});
|
|
226
434
|
});
|
|
227
435
|
}
|
package/dist/version.js
CHANGED
package/dist/view.js
CHANGED
|
@@ -70,7 +70,11 @@ export function fullEvent(e) {
|
|
|
70
70
|
...compactEvent(e),
|
|
71
71
|
ShowAs: e.ShowAs,
|
|
72
72
|
Attendees: e.Attendees?.map((a) => pruned({ Who: addr(a), Response: a.Status?.Response })),
|
|
73
|
-
OnlineMeetingUrl
|
|
73
|
+
// The Teams link lives under OnlineMeeting; OnlineMeetingUrl is empty on
|
|
74
|
+
// every live Teams meeting checked.
|
|
75
|
+
JoinUrl: e.OnlineMeeting?.JoinUrl || e.OnlineMeetingUrl || undefined,
|
|
76
|
+
// Kept beside JoinUrl so a consumer of the old field still finds it.
|
|
77
|
+
OnlineMeetingUrl: e.OnlineMeetingUrl || undefined,
|
|
74
78
|
Preview: e.BodyPreview?.trim() || undefined,
|
|
75
79
|
WebLink: e.WebLink,
|
|
76
80
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chrischall/office-outlook-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"mcpName": "io.github.chrischall/office-outlook-mcp",
|
|
5
5
|
"description": "Outlook / Microsoft 365 MCP server for Claude — developed and maintained by AI (Claude Code)",
|
|
6
6
|
"author": "Claude Code (AI) <https://www.anthropic.com/claude>",
|
|
@@ -45,8 +45,8 @@
|
|
|
45
45
|
"typecheck": "tsc -p tsconfig.check.json"
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
|
-
"@chrischall/mcp-utils": "^2.
|
|
49
|
-
"@fetchproxy/server": "^3.
|
|
48
|
+
"@chrischall/mcp-utils": "^2.15.0",
|
|
49
|
+
"@fetchproxy/server": "^3.6.0",
|
|
50
50
|
"@modelcontextprotocol/server": "^2.3.0",
|
|
51
51
|
"dotenv": "^18.0.1",
|
|
52
52
|
"zod": "^4.6.5"
|
package/server.json
CHANGED
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
"url": "https://github.com/chrischall/office-outlook-mcp",
|
|
7
7
|
"source": "github"
|
|
8
8
|
},
|
|
9
|
-
"version": "0.
|
|
9
|
+
"version": "0.4.0",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "@chrischall/office-outlook-mcp",
|
|
14
|
-
"version": "0.
|
|
14
|
+
"version": "0.4.0",
|
|
15
15
|
"transport": {
|
|
16
16
|
"type": "stdio"
|
|
17
17
|
},
|