@chrischall/office-outlook-mcp 0.0.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.
@@ -0,0 +1,130 @@
1
+ import { z } from 'zod';
2
+ import { minifiedResult, resolveView, viewParam, McpToolError } from '@chrischall/mcp-utils';
3
+ import { compactFolder, compactMessage, fullMessage, projectCollection, } from '../view.js';
4
+ export const VIEWS = ['compact', 'full', 'raw'];
5
+ /**
6
+ * Well-known folder names the API accepts wherever a folder id is taken.
7
+ * All eight verified present on a live mailbox 2026-09-20.
8
+ */
9
+ const WELL_KNOWN = [
10
+ 'inbox',
11
+ 'drafts',
12
+ 'sentitems',
13
+ 'deleteditems',
14
+ 'archive',
15
+ 'junkemail',
16
+ 'outbox',
17
+ 'clutter',
18
+ ];
19
+ /** Fields worth listing. Anything not here costs bytes on every row. */
20
+ const LIST_SELECT = 'Id,Subject,From,ToRecipients,ReceivedDateTime,IsRead,HasAttachments,BodyPreview';
21
+ function qs(params) {
22
+ const parts = [];
23
+ for (const [k, v] of Object.entries(params)) {
24
+ if (v === undefined || v === '')
25
+ continue;
26
+ parts.push(`${k}=${encodeURIComponent(String(v))}`);
27
+ }
28
+ return parts.length ? `?${parts.join('&')}` : '';
29
+ }
30
+ /** `inbox` etc. pass through; anything else is treated as an opaque folder id. */
31
+ function folderSegment(folder) {
32
+ return encodeURIComponent(folder);
33
+ }
34
+ export function registerMailTools(server, client) {
35
+ server.registerTool('outlook_list_folders', {
36
+ description: 'List mail folders with unread and total counts. Use this to discover folder ids before listing messages; the well-known names (' +
37
+ WELL_KNOWN.join(', ') +
38
+ ') can be used directly without a lookup.',
39
+ annotations: { readOnlyHint: true },
40
+ inputSchema: z.object({
41
+ view: viewParam(VIEWS),
42
+ limit: z.number().int().min(1).max(200).optional().describe('Max folders (default 50)'),
43
+ }),
44
+ }, async ({ view, limit }) => {
45
+ const data = await client.get(`/me/mailfolders${qs({ $top: limit ?? 50 })}`);
46
+ if (resolveView(view, VIEWS) === 'raw')
47
+ return minifiedResult(data);
48
+ return minifiedResult(projectCollection(data, compactFolder, 'mail folder'));
49
+ });
50
+ server.registerTool('outlook_list_messages', {
51
+ description: "List messages in a folder, newest first. Defaults to the inbox. Use `unreadOnly` for a triage view. Bodies are NOT included — call outlook_get_message for one. Note `search` and `unreadOnly` cannot be combined (the API rejects $search with $filter); pass `search` alone to search the whole mailbox.",
52
+ annotations: { readOnlyHint: true },
53
+ inputSchema: z.object({
54
+ view: viewParam(VIEWS),
55
+ folder: z
56
+ .string()
57
+ .optional()
58
+ .describe('Folder id or well-known name (default "inbox")'),
59
+ limit: z.number().int().min(1).max(100).optional().describe('Max messages (default 25)'),
60
+ skip: z.number().int().min(0).optional().describe('Offset for paging'),
61
+ unreadOnly: z.boolean().optional().describe('Only unread messages'),
62
+ search: z
63
+ .string()
64
+ .optional()
65
+ .describe('Full-text search across the whole mailbox; cannot combine with unreadOnly'),
66
+ }),
67
+ }, async ({ view, folder, limit, skip, unreadOnly, search }) => {
68
+ if (search !== undefined && unreadOnly === true) {
69
+ throw new McpToolError('`search` and `unreadOnly` cannot be combined.', {
70
+ hint: 'Outlook rejects $search together with $filter. Search first, then filter the results.',
71
+ });
72
+ }
73
+ // $search always spans the mailbox; scoping it to a folder is not supported.
74
+ const base = search !== undefined ? '/me/messages' : `/me/mailfolders/${folderSegment(folder ?? 'inbox')}/messages`;
75
+ const params = {
76
+ $top: limit ?? 25,
77
+ $skip: skip,
78
+ $select: LIST_SELECT,
79
+ };
80
+ if (search !== undefined) {
81
+ params.$search = `"${search.replace(/"/g, '')}"`;
82
+ }
83
+ else {
84
+ // $orderby is rejected alongside $search, so it is set only here.
85
+ params.$orderby = 'ReceivedDateTime desc';
86
+ if (unreadOnly === true)
87
+ params.$filter = 'IsRead eq false';
88
+ }
89
+ const data = await client.get(`${base}${qs(params)}`);
90
+ const v = resolveView(view, VIEWS);
91
+ if (v === 'raw')
92
+ return minifiedResult(data);
93
+ return minifiedResult(projectCollection(data, v === 'full' ? fullMessage : compactMessage, 'message'));
94
+ });
95
+ server.registerTool('outlook_get_message', {
96
+ description: 'Get one message including its body. The body is requested as plain text rather than HTML (measured ~9x smaller and far easier to read); pass view:"raw" to get Outlook\'s untouched record.',
97
+ annotations: { readOnlyHint: true },
98
+ inputSchema: z.object({
99
+ view: viewParam(VIEWS),
100
+ id: z.string().min(1).describe('Message Id from outlook_list_messages'),
101
+ html: z
102
+ .boolean()
103
+ .optional()
104
+ .describe('Return the HTML body instead of plain text (default false)'),
105
+ }),
106
+ }, async ({ view, id, html }) => {
107
+ const data = await client.get(`/me/messages/${encodeURIComponent(id)}`, {
108
+ text: html !== true,
109
+ });
110
+ const v = resolveView(view, VIEWS);
111
+ if (v === 'raw')
112
+ return minifiedResult(data);
113
+ return minifiedResult(v === 'full' ? fullMessage(data) : compactMessage(data));
114
+ });
115
+ server.registerTool('outlook_list_attachments', {
116
+ description: "List a message's attachments (name, size, content type). Metadata only — this deliberately does not download bytes.",
117
+ annotations: { readOnlyHint: true },
118
+ inputSchema: z.object({
119
+ id: z.string().min(1).describe('Message Id'),
120
+ }),
121
+ }, async ({ id }) => {
122
+ const data = await client.get(`/me/messages/${encodeURIComponent(id)}/attachments${qs({
123
+ $select: 'Id,Name,Size,ContentType',
124
+ })}`);
125
+ return minifiedResult({
126
+ count: data.value?.length ?? 0,
127
+ items: data.value ?? [],
128
+ });
129
+ });
130
+ }
@@ -0,0 +1,165 @@
1
+ import { z } from 'zod';
2
+ import { minifiedResult } from '@chrischall/mcp-utils';
3
+ import { previewUnlessConfirmed, schemaConfirm } from './_confirm.js';
4
+ const recipientList = z
5
+ .array(z.string().min(3))
6
+ .optional()
7
+ .describe('Email addresses');
8
+ function toRecipients(list) {
9
+ return (list ?? []).map((Address) => ({ EmailAddress: { Address } }));
10
+ }
11
+ /**
12
+ * Body shape for a message. `ContentType` is `Text` by default — an agent
13
+ * composing prose should not have to emit HTML, and Outlook renders text fine.
14
+ */
15
+ function messageBody(subject, body, html) {
16
+ return {
17
+ Subject: subject,
18
+ Body: { ContentType: html === true ? 'HTML' : 'Text', Content: body },
19
+ };
20
+ }
21
+ export function registerWriteTools(server, client) {
22
+ server.registerTool('outlook_send_mail', {
23
+ description: 'Send an email from the signed-in mailbox. Requires confirm:true — without it this makes no network call and returns a preview of exactly what would be sent.',
24
+ annotations: { readOnlyHint: false, destructiveHint: false },
25
+ inputSchema: z.object({
26
+ to: recipientList,
27
+ cc: recipientList,
28
+ bcc: recipientList,
29
+ subject: z.string().describe('Subject line'),
30
+ body: z.string().describe('Message body'),
31
+ html: z.boolean().optional().describe('Send the body as HTML (default: plain text)'),
32
+ saveToSentItems: z.boolean().optional().describe('Keep a copy in Sent Items (default true)'),
33
+ confirm: schemaConfirm,
34
+ }),
35
+ }, async ({ to, cc, bcc, subject, body, html, saveToSentItems, confirm }) => {
36
+ const payload = {
37
+ Message: {
38
+ ...messageBody(subject, body, html),
39
+ ToRecipients: toRecipients(to),
40
+ ...(cc?.length ? { CcRecipients: toRecipients(cc) } : {}),
41
+ ...(bcc?.length ? { BccRecipients: toRecipients(bcc) } : {}),
42
+ },
43
+ SaveToSentItems: saveToSentItems !== false,
44
+ };
45
+ const recipients = [...(to ?? []), ...(cc ?? []), ...(bcc ?? [])].join(', ');
46
+ const gate = previewUnlessConfirmed(confirm, `Send mail "${subject}" to ${recipients || '(no recipients)'}`, 'POST', '/me/sendmail', payload);
47
+ if (gate)
48
+ return gate;
49
+ await client.write('POST', '/me/sendmail', payload);
50
+ // sendmail returns 202 with an empty body; there is no id to report and
51
+ // no resource to re-read, so say exactly what is known.
52
+ return minifiedResult({ sent: true, subject, recipients: recipients || null });
53
+ });
54
+ server.registerTool('outlook_create_draft', {
55
+ description: 'Create a draft message in the Drafts folder without sending it. Requires confirm:true. Returns the created draft, which can be reviewed and sent from Outlook.',
56
+ annotations: { readOnlyHint: false, destructiveHint: false },
57
+ inputSchema: z.object({
58
+ to: recipientList,
59
+ cc: recipientList,
60
+ subject: z.string().describe('Subject line'),
61
+ body: z.string().describe('Message body'),
62
+ html: z.boolean().optional().describe('Compose the body as HTML (default: plain text)'),
63
+ confirm: schemaConfirm,
64
+ }),
65
+ }, async ({ to, cc, subject, body, html, confirm }) => {
66
+ const payload = {
67
+ ...messageBody(subject, body, html),
68
+ ToRecipients: toRecipients(to),
69
+ ...(cc?.length ? { CcRecipients: toRecipients(cc) } : {}),
70
+ };
71
+ const gate = previewUnlessConfirmed(confirm, `Create draft "${subject}"`, 'POST', '/me/messages', payload);
72
+ if (gate)
73
+ return gate;
74
+ const created = await client.write('POST', '/me/messages', payload);
75
+ return minifiedResult({ created: true, Id: created?.Id, WebLink: created?.WebLink });
76
+ });
77
+ server.registerTool('outlook_mark_read', {
78
+ description: 'Mark a message read or unread. Requires confirm:true. The result is verified by re-reading the message — a 2xx alone is not proof it persisted.',
79
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true },
80
+ inputSchema: z.object({
81
+ id: z.string().min(1).describe('Message Id'),
82
+ isRead: z.boolean().describe('true to mark read, false to mark unread'),
83
+ confirm: schemaConfirm,
84
+ }),
85
+ }, async ({ id, isRead, confirm }) => {
86
+ const path = `/me/messages/${encodeURIComponent(id)}`;
87
+ const payload = { IsRead: isRead };
88
+ const gate = previewUnlessConfirmed(confirm, `Mark message ${id} as ${isRead ? 'read' : 'unread'}`, 'PATCH', path, payload);
89
+ if (gate)
90
+ return gate;
91
+ await client.write('PATCH', path, payload);
92
+ // Re-read rather than trust the status. IsRead is the field the write
93
+ // requested, so it is the field that proves it.
94
+ const after = await client.get(`${path}?$select=IsRead`);
95
+ return minifiedResult({
96
+ updated: after?.IsRead === isRead,
97
+ IsRead: after?.IsRead,
98
+ ...(after?.IsRead === isRead ? {} : { warning: 'Outlook accepted the write but the value did not change.' }),
99
+ });
100
+ });
101
+ server.registerTool('outlook_move_message', {
102
+ description: 'Move a message to another folder (e.g. "archive", "deleteditems", or a folder id from outlook_list_folders). Requires confirm:true. Moving assigns a NEW message id, which is returned.',
103
+ annotations: { readOnlyHint: false, destructiveHint: false },
104
+ inputSchema: z.object({
105
+ id: z.string().min(1).describe('Message Id'),
106
+ destination: z
107
+ .string()
108
+ .min(1)
109
+ .describe('Destination folder id or well-known name (e.g. "archive")'),
110
+ confirm: schemaConfirm,
111
+ }),
112
+ }, async ({ id, destination, confirm }) => {
113
+ const path = `/me/messages/${encodeURIComponent(id)}/move`;
114
+ const payload = { DestinationId: destination };
115
+ const gate = previewUnlessConfirmed(confirm, `Move message ${id} to ${destination}`, 'POST', path, payload);
116
+ if (gate)
117
+ return gate;
118
+ const moved = await client.write('POST', path, payload);
119
+ return minifiedResult({
120
+ moved: true,
121
+ newId: moved?.Id,
122
+ ParentFolderId: moved?.ParentFolderId,
123
+ note: 'The message has a new Id after a move; the old one no longer resolves.',
124
+ });
125
+ });
126
+ server.registerTool('outlook_create_event', {
127
+ description: 'Create a calendar event. Requires confirm:true. `timeZone` takes a WINDOWS zone name such as "Eastern Standard Time", not an IANA name.',
128
+ annotations: { readOnlyHint: false, destructiveHint: false },
129
+ inputSchema: z.object({
130
+ subject: z.string().describe('Event title'),
131
+ start: z.string().min(1).describe('Start, ISO 8601 local time e.g. 2026-09-22T15:00:00'),
132
+ end: z.string().min(1).describe('End, ISO 8601 local time'),
133
+ timeZone: z
134
+ .string()
135
+ .optional()
136
+ .describe('Windows time-zone name (default "UTC")'),
137
+ location: z.string().optional().describe('Location display name'),
138
+ body: z.string().optional().describe('Event description'),
139
+ attendees: recipientList,
140
+ confirm: schemaConfirm,
141
+ }),
142
+ }, async ({ subject, start, end, timeZone, location, body, attendees, confirm }) => {
143
+ const tz = timeZone ?? 'UTC';
144
+ const payload = {
145
+ Subject: subject,
146
+ Start: { DateTime: start, TimeZone: tz },
147
+ End: { DateTime: end, TimeZone: tz },
148
+ ...(location ? { Location: { DisplayName: location } } : {}),
149
+ ...(body ? { Body: { ContentType: 'Text', Content: body } } : {}),
150
+ ...(attendees?.length
151
+ ? {
152
+ Attendees: attendees.map((Address) => ({
153
+ EmailAddress: { Address },
154
+ Type: 'Required',
155
+ })),
156
+ }
157
+ : {}),
158
+ };
159
+ const gate = previewUnlessConfirmed(confirm, `Create event "${subject}" ${start} to ${end} (${tz})`, 'POST', '/me/events', payload);
160
+ if (gate)
161
+ return gate;
162
+ const created = await client.write('POST', '/me/events', payload);
163
+ return minifiedResult({ created: true, Id: created?.Id, WebLink: created?.WebLink });
164
+ });
165
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * The single source of the package version.
3
+ *
4
+ * Everything that needs a version imports from here, so release-please has one
5
+ * file to rewrite and there is no second marker to forget. Registered in
6
+ * `release-please-config.json` under `extra-files`.
7
+ *
8
+ * Note for anyone editing the comments in this file: `versionSyncTest` flags
9
+ * every line containing the marker string, so it must appear on the export
10
+ * line below and nowhere else — not even in prose.
11
+ */
12
+ export const PACKAGE_NAME = 'office-outlook-mcp';
13
+ export const VERSION = '0.0.0'; // x-release-please-version
package/dist/view.js ADDED
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Response projections.
3
+ *
4
+ * Outlook's default projection is ~40 fields per message including the full
5
+ * body, which is a great deal of context for an agent that is browsing. Every
6
+ * read tool therefore takes `view` and defaults to `compact`.
7
+ *
8
+ * The projections below name only fields observed on live responses
9
+ * (2026-09-20). Anything unrecognised falls through to the raw value rather
10
+ * than being dropped — a record with holes in it is indistinguishable from
11
+ * "there was nothing there".
12
+ */
13
+ import { projectOrRaw } from '@chrischall/mcp-utils';
14
+ function addr(r) {
15
+ const e = r?.EmailAddress;
16
+ if (!e)
17
+ return undefined;
18
+ if (e.Name && e.Address && e.Name !== e.Address)
19
+ return `${e.Name} <${e.Address}>`;
20
+ return e.Address ?? e.Name;
21
+ }
22
+ function addrs(list) {
23
+ if (!Array.isArray(list) || list.length === 0)
24
+ return undefined;
25
+ return list.map((r) => addr(r)).filter((s) => s !== undefined);
26
+ }
27
+ export function compactMessage(m) {
28
+ return pruned({
29
+ Id: m.Id,
30
+ Subject: m.Subject,
31
+ From: addr(m.From ?? m.Sender),
32
+ To: addrs(m.ToRecipients),
33
+ Received: m.ReceivedDateTime,
34
+ IsRead: m.IsRead,
35
+ HasAttachments: m.HasAttachments || undefined,
36
+ Preview: m.BodyPreview?.trim() || undefined,
37
+ // Present only when the caller asked for a single message; a listing
38
+ // projection would otherwise carry every body.
39
+ Body: m.Body?.Content,
40
+ });
41
+ }
42
+ export function fullMessage(m) {
43
+ return pruned({
44
+ ...compactMessage(m),
45
+ Cc: addrs(m.CcRecipients),
46
+ Sent: m.SentDateTime,
47
+ Importance: m.Importance,
48
+ IsDraft: m.IsDraft,
49
+ ConversationId: m.ConversationId,
50
+ ParentFolderId: m.ParentFolderId,
51
+ Categories: m.Categories?.length ? m.Categories : undefined,
52
+ WebLink: m.WebLink,
53
+ });
54
+ }
55
+ export function compactEvent(e) {
56
+ return pruned({
57
+ Id: e.Id,
58
+ Subject: e.Subject,
59
+ Start: e.Start?.DateTime,
60
+ End: e.End?.DateTime,
61
+ TimeZone: e.Start?.TimeZone,
62
+ Location: e.Location?.DisplayName || undefined,
63
+ Organizer: addr(e.Organizer),
64
+ IsAllDay: e.IsAllDay || undefined,
65
+ IsCancelled: e.IsCancelled || undefined,
66
+ });
67
+ }
68
+ export function fullEvent(e) {
69
+ return pruned({
70
+ ...compactEvent(e),
71
+ ShowAs: e.ShowAs,
72
+ Attendees: e.Attendees?.map((a) => pruned({ Who: addr(a), Response: a.Status?.Response })),
73
+ OnlineMeetingUrl: e.OnlineMeetingUrl,
74
+ Preview: e.BodyPreview?.trim() || undefined,
75
+ WebLink: e.WebLink,
76
+ });
77
+ }
78
+ export function compactFolder(f) {
79
+ return pruned({
80
+ Id: f.Id,
81
+ Name: f.DisplayName,
82
+ Unread: f.UnreadItemCount,
83
+ Total: f.TotalItemCount,
84
+ });
85
+ }
86
+ /** Drop `undefined` so a compact record does not carry empty keys. */
87
+ function pruned(o) {
88
+ const out = {};
89
+ for (const [k, v] of Object.entries(o)) {
90
+ if (v !== undefined)
91
+ out[k] = v;
92
+ }
93
+ return out;
94
+ }
95
+ /**
96
+ * Apply a projection to an OData collection envelope, preserving paging.
97
+ *
98
+ * `projectOrRaw` hands back the raw value when the projection throws or yields
99
+ * nothing, so a shape we did not anticipate degrades to "everything" rather
100
+ * than to "nothing".
101
+ */
102
+ export function projectCollection(payload, project, label) {
103
+ const items = Array.isArray(payload?.value) ? payload.value : [];
104
+ return pruned({
105
+ count: items.length,
106
+ items: items.map((item) => projectOrRaw(item, project, { label, context: 'office-outlook-mcp' })),
107
+ nextLink: payload?.['@odata.nextLink'],
108
+ });
109
+ }
package/mint.yaml ADDED
@@ -0,0 +1,79 @@
1
+ version: 1
2
+ name: Outlook (Microsoft 365)
3
+ slug: outlook
4
+ summary: >-
5
+ Outlook / Microsoft 365 for Claude — read mail, folders, calendar, contacts
6
+ and tasks, and send mail with confirmation
7
+ #
8
+ # Hosting note (a comment, not user-facing summary text): this MCP mints its
9
+ # credential through the fetchproxy BROWSER BRIDGE. It is not a full-bridge
10
+ # server — every API request is a plain server-side fetch to
11
+ # outlook.office.com — but the bearer token it needs can only come from a
12
+ # signed-in Outlook tab, and tokens last only ~25h, so a hosted registration
13
+ # that is expected to keep working needs the bridge.
14
+ #
15
+ # A bridged registration also needs runtime `fly-shared`, `bridge: true` and a
16
+ # `bridgePortEnv`, which are REGISTRATION fields this manifest has no schema
17
+ # for — set them over the control API (PUT on the registration id). The port
18
+ # variable to name in `bridgePortEnv` is OUTLOOK_WS_PORT, declared below.
19
+ # `state.dataDir` below is required for `bridge`.
20
+ #
21
+ # The alternative, for a short-lived or CI registration, is to supply
22
+ # OUTLOOK_ACCESS_TOKEN as a principal secret and accept that it expires in
23
+ # about a day.
24
+ #
25
+ # Credentials here are PER PERSON — this reads someone's work mailbox — so a
26
+ # real deployment wants identity.perUserChild with OUTLOOK_ACCESS_TOKEN as an
27
+ # auth field, never one operator's token shared via --secret-env.
28
+ env:
29
+ - name: OUTLOOK_ACCESS_TOKEN
30
+ secret: true
31
+ required: false
32
+ help: >-
33
+ Pre-obtained Outlook bearer token. Overrides the browser bridge. Power
34
+ users / CI only; tokens last about 25 hours.
35
+ - name: OUTLOOK_DISABLE_FETCHPROXY
36
+ required: false
37
+ help: >-
38
+ Set to '1' to disable browser capture, requiring OUTLOOK_ACCESS_TOKEN.
39
+ - name: OUTLOOK_API_BASE
40
+ required: false
41
+ help: >-
42
+ Override the Outlook REST base URL. Defaults to
43
+ https://outlook.office.com/api/v2.0.
44
+ - name: OUTLOOK_WS_PORT
45
+ required: false
46
+ help: >-
47
+ fetchproxy concentrator port used for token capture. Defaults to 37149,
48
+ the fleet-wide shared port. Name this variable in the registration's
49
+ bridgePortEnv for a bridged deployment.
50
+ - name: OUTLOOK_CAPTURE_TIMEOUT
51
+ required: false
52
+ help: >-
53
+ Seconds to wait for the Outlook tab to make a request we can read a token
54
+ from. Defaults to 30.
55
+ - name: OUTLOOK_TOKEN_CACHE
56
+ required: false
57
+ help: >-
58
+ Set to 'false' to stop caching the captured token between runs. Defaults
59
+ to true.
60
+ - name: OUTLOOK_TOKEN_FILE
61
+ required: false
62
+ help: >-
63
+ Path to the cached token file. Defaults to
64
+ ~/.office-outlook-mcp/token.json.
65
+ state:
66
+ dataDir: true
67
+ reason: >-
68
+ Caches the captured bearer token at ~/.office-outlook-mcp/token.json (0600)
69
+ and, for a bridged registration, holds the fetchproxy identity keypair
70
+ under ~/.fetchproxy/identity. Without persistence every cold start
71
+ re-captures from the browser — and on a scale-to-zero machine that means
72
+ re-prompting the user to approve pairing, constantly.
73
+ egress:
74
+ # The ONLY host this server fetches. The bridge's declared domains
75
+ # (cloud.microsoft, office.com) are what the BROWSER reaches during token
76
+ # capture, not what this Node process requests, so they are deliberately not
77
+ # listed here.
78
+ allow:
79
+ - outlook.office.com
package/package.json ADDED
@@ -0,0 +1,62 @@
1
+ {
2
+ "name": "@chrischall/office-outlook-mcp",
3
+ "version": "0.0.0",
4
+ "mcpName": "io.github.chrischall/office-outlook-mcp",
5
+ "description": "Outlook / Microsoft 365 MCP server for Claude \u2014 developed and maintained by AI (Claude Code)",
6
+ "author": "Claude Code (AI) <https://www.anthropic.com/claude>",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/chrischall/office-outlook-mcp.git"
10
+ },
11
+ "license": "MIT",
12
+ "publishConfig": {
13
+ "access": "public"
14
+ },
15
+ "keywords": [
16
+ "mcp",
17
+ "model-context-protocol",
18
+ "claude",
19
+ "ai",
20
+ "outlook",
21
+ "office365",
22
+ "microsoft365",
23
+ "email",
24
+ "calendar"
25
+ ],
26
+ "type": "module",
27
+ "bin": {
28
+ "office-outlook-mcp": "dist/bundle.js"
29
+ },
30
+ "files": [
31
+ "dist",
32
+ ".claude-plugin",
33
+ ".mcp.json",
34
+ "server.json",
35
+ "skills",
36
+ "mint.yaml"
37
+ ],
38
+ "scripts": {
39
+ "build": "tsc && npm run bundle",
40
+ "bundle": "esbuild src/index.ts --bundle --platform=node --format=esm --external:dotenv --banner:js='import { createRequire as __createRequire } from \"module\"; const require = __createRequire(import.meta.url);' --outfile=dist/bundle.js",
41
+ "dev": "node dist/index.js",
42
+ "test": "npm run typecheck && vitest run",
43
+ "test:watch": "vitest",
44
+ "test:coverage": "npm run typecheck && vitest run --coverage",
45
+ "typecheck": "tsc -p tsconfig.check.json"
46
+ },
47
+ "dependencies": {
48
+ "@chrischall/mcp-utils": "^2.0.0",
49
+ "@fetchproxy/server": "^3.0.1",
50
+ "@modelcontextprotocol/server": "^2.0.0",
51
+ "dotenv": "^18.0.1",
52
+ "zod": "^4.6.2"
53
+ },
54
+ "devDependencies": {
55
+ "@modelcontextprotocol/client": "^2.0.0",
56
+ "@types/node": "^26.0.0",
57
+ "@vitest/coverage-v8": "^5.0.0",
58
+ "esbuild": "^0.28.1",
59
+ "typescript": "^7.0.2",
60
+ "vitest": "^5.0.0"
61
+ }
62
+ }
package/server.json ADDED
@@ -0,0 +1,63 @@
1
+ {
2
+ "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
3
+ "name": "io.github.chrischall/office-outlook-mcp",
4
+ "description": "Outlook / Microsoft 365 for Claude — mail, calendar, contacts and tasks, with confirm-gated sending",
5
+ "repository": {
6
+ "url": "https://github.com/chrischall/office-outlook-mcp",
7
+ "source": "github"
8
+ },
9
+ "version": "0.0.0",
10
+ "packages": [
11
+ {
12
+ "registryType": "npm",
13
+ "identifier": "@chrischall/office-outlook-mcp",
14
+ "version": "0.0.0",
15
+ "transport": { "type": "stdio" },
16
+ "environmentVariables": [
17
+ {
18
+ "name": "OUTLOOK_ACCESS_TOKEN",
19
+ "description": "Pre-obtained Outlook bearer token. Overrides the browser bridge. Power users / CI only; tokens last about 25 hours.",
20
+ "isRequired": false,
21
+ "format": "string",
22
+ "isSecret": true
23
+ },
24
+ {
25
+ "name": "OUTLOOK_DISABLE_FETCHPROXY",
26
+ "description": "Set to '1' to disable browser capture, requiring OUTLOOK_ACCESS_TOKEN.",
27
+ "isRequired": false,
28
+ "format": "string"
29
+ },
30
+ {
31
+ "name": "OUTLOOK_API_BASE",
32
+ "description": "Override the Outlook REST base URL. Defaults to https://outlook.office.com/api/v2.0.",
33
+ "isRequired": false,
34
+ "format": "string"
35
+ },
36
+ {
37
+ "name": "OUTLOOK_WS_PORT",
38
+ "description": "fetchproxy concentrator port used for token capture. Defaults to 37149, the fleet-wide shared port.",
39
+ "isRequired": false,
40
+ "format": "string"
41
+ },
42
+ {
43
+ "name": "OUTLOOK_CAPTURE_TIMEOUT",
44
+ "description": "Seconds to wait for the Outlook tab to make a request we can read a token from. Defaults to 30.",
45
+ "isRequired": false,
46
+ "format": "string"
47
+ },
48
+ {
49
+ "name": "OUTLOOK_TOKEN_CACHE",
50
+ "description": "Set to 'false' to stop caching the captured token between runs. Defaults to true.",
51
+ "isRequired": false,
52
+ "format": "string"
53
+ },
54
+ {
55
+ "name": "OUTLOOK_TOKEN_FILE",
56
+ "description": "Path to the cached token file. Defaults to ~/.office-outlook-mcp/token.json.",
57
+ "isRequired": false,
58
+ "format": "string"
59
+ }
60
+ ]
61
+ }
62
+ ]
63
+ }