@joshluedeman/m365-mcp 0.1.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/LICENSE +21 -0
- package/README.md +196 -0
- package/dist/chunk-JEMHJMEL.js +207 -0
- package/dist/chunk-JEMHJMEL.js.map +1 -0
- package/dist/index.cjs +1183 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.js +796 -0
- package/dist/index.js.map +1 -0
- package/dist/setup-K6EYBEFH.js +72 -0
- package/dist/setup-K6EYBEFH.js.map +1 -0
- package/package.json +66 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Josh Luedeman
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
# 📬 m365-mcp
|
|
2
|
+
|
|
3
|
+
**Microsoft 365 tools for any MCP-compatible AI client — Mail, Calendar, Tasks, and Contacts via the Microsoft Graph API.**
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/@joshluedeman/m365-mcp)
|
|
6
|
+
[](./LICENSE)
|
|
7
|
+
[](https://github.com/joshluedeman/m365-mcp/actions)
|
|
8
|
+
[](https://www.npmjs.com/package/@joshluedeman/m365-mcp)
|
|
9
|
+
|
|
10
|
+
`m365-mcp` is a [Model Context Protocol](https://modelcontextprotocol.io) server that connects your Microsoft 365 account to any MCP-compatible AI client. It exposes 22 tools across Mail, Calendar, Tasks (Microsoft To Do), and Contacts — all authenticated via a single device code sign-in with no Azure portal setup required. It runs over stdio and works with Claude Desktop, Claude Code, Cursor, Zed, Windsurf, and any other MCP-compatible client.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## ✨ Features
|
|
15
|
+
|
|
16
|
+
- 📧 **Mail** — search, read, send, flag, and organize email across folders
|
|
17
|
+
- 📅 **Calendar** — search events, create/update meetings, find free/busy availability
|
|
18
|
+
- ✅ **Tasks** — full Microsoft To Do integration: task lists, tasks, create/complete/delete
|
|
19
|
+
- 👤 **Contacts** — search, read, create, and update personal contacts
|
|
20
|
+
- 🔐 **Zero Azure setup** — ships with a pre-registered multi-tenant app; no portal configuration required
|
|
21
|
+
- 📦 **npx distribution** — run with `npx @joshluedeman/m365-mcp`, no global install needed
|
|
22
|
+
- 🖥 **Any MCP client** — Claude Desktop, Claude Code, Cursor, Zed, Windsurf, and more
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## 🏗 How it works
|
|
27
|
+
|
|
28
|
+
```mermaid
|
|
29
|
+
graph LR
|
|
30
|
+
User["User"] --> Client["MCP Client"]
|
|
31
|
+
Client -->|"MCP stdio"| Server["m365-mcp"]
|
|
32
|
+
Server -->|"HTTPS"| Graph["Microsoft Graph API"]
|
|
33
|
+
Graph --> Mail["Mail"]
|
|
34
|
+
Graph --> Calendar["Calendar"]
|
|
35
|
+
Graph --> Tasks["Tasks"]
|
|
36
|
+
Graph --> Contacts["Contacts"]
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## 🚀 Quick Start
|
|
42
|
+
|
|
43
|
+
**1. (Optional) Install globally**
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
npm install -g @joshluedeman/m365-mcp
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Or skip this and use `npx` directly — it works either way.
|
|
50
|
+
|
|
51
|
+
**2. Run setup**
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
npx @joshluedeman/m365-mcp setup
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
This opens a browser for device code sign-in. Sign in with your Microsoft 365 account and the token is cached to `~/.config/m365-mcp/token-cache.json`. You will not be prompted again unless the refresh token expires (~90 days).
|
|
58
|
+
|
|
59
|
+
**3. Add to your MCP client**
|
|
60
|
+
|
|
61
|
+
Most MCP clients accept a JSON server definition in this form:
|
|
62
|
+
|
|
63
|
+
```json
|
|
64
|
+
{
|
|
65
|
+
"mcpServers": {
|
|
66
|
+
"m365": {
|
|
67
|
+
"command": "npx",
|
|
68
|
+
"args": ["m365-mcp"]
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
**Claude Desktop** — edit `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or `%APPDATA%\Claude\claude_desktop_config.json` (Windows).
|
|
75
|
+
|
|
76
|
+
**Claude Code** — add via CLI:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
claude mcp add m365 -- npx @joshluedeman/m365-mcp
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
**Cursor** — add to `.cursor/mcp.json` in your project or `~/.cursor/mcp.json` globally.
|
|
83
|
+
|
|
84
|
+
**Zed** — add to your Zed settings under `"context_servers"`.
|
|
85
|
+
|
|
86
|
+
**Other clients** — consult your client's MCP server configuration docs; the command is `npx @joshluedeman/m365-mcp`.
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## 🔐 Authentication
|
|
91
|
+
|
|
92
|
+
Authentication uses the [MSAL device code flow](https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-device-code). No Azure CLI, no service principal, no secrets to manage.
|
|
93
|
+
|
|
94
|
+
```mermaid
|
|
95
|
+
sequenceDiagram
|
|
96
|
+
participant U as User
|
|
97
|
+
participant S as m365-mcp setup
|
|
98
|
+
participant A as Azure AD
|
|
99
|
+
|
|
100
|
+
U->>S: npx @joshluedeman/m365-mcp setup
|
|
101
|
+
S->>A: Request device code
|
|
102
|
+
A-->>S: Device code + verification URL
|
|
103
|
+
S-->>U: Display URL and code
|
|
104
|
+
U->>A: Open browser, enter code, sign in
|
|
105
|
+
A-->>S: Access token + refresh token
|
|
106
|
+
S->>S: Cache tokens to ~/.config/m365-mcp/token-cache.json
|
|
107
|
+
S-->>U: Done — setup complete
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
On subsequent runs the server loads the cached token silently. The refresh token is valid for approximately 90 days; re-run `npx @joshluedeman/m365-mcp setup` if it expires.
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
## 🛠 Available Tools
|
|
115
|
+
|
|
116
|
+
### Mail
|
|
117
|
+
|
|
118
|
+
| Tool | Description |
|
|
119
|
+
|------|-------------|
|
|
120
|
+
| `search_emails` | Search emails by keyword, sender, date range, or folder |
|
|
121
|
+
| `read_email` | Read the full content of an email by ID |
|
|
122
|
+
| `send_email` | Send a new email |
|
|
123
|
+
| `flag_email` | Flag or unflag an email for follow-up |
|
|
124
|
+
| `list_mail_folders` | List all mail folders in the mailbox |
|
|
125
|
+
| `move_email` | Move an email to a different folder |
|
|
126
|
+
|
|
127
|
+
### Calendar
|
|
128
|
+
|
|
129
|
+
| Tool | Description |
|
|
130
|
+
|------|-------------|
|
|
131
|
+
| `search_events` | Search calendar events by keyword or time range |
|
|
132
|
+
| `get_event` | Get full details of a calendar event by ID |
|
|
133
|
+
| `create_event` | Create a new calendar event |
|
|
134
|
+
| `update_event` | Update an existing calendar event |
|
|
135
|
+
| `find_availability` | Find free/busy availability across attendees |
|
|
136
|
+
|
|
137
|
+
### Tasks (Microsoft To Do)
|
|
138
|
+
|
|
139
|
+
| Tool | Description |
|
|
140
|
+
|------|-------------|
|
|
141
|
+
| `list_task_lists` | List all task lists |
|
|
142
|
+
| `list_tasks` | List tasks in a task list |
|
|
143
|
+
| `get_task` | Get details of a specific task |
|
|
144
|
+
| `create_task` | Create a new task in a task list |
|
|
145
|
+
| `update_task` | Update an existing task |
|
|
146
|
+
| `complete_task` | Mark a task as completed |
|
|
147
|
+
| `delete_task` | Delete a task |
|
|
148
|
+
|
|
149
|
+
### Contacts
|
|
150
|
+
|
|
151
|
+
| Tool | Description |
|
|
152
|
+
|------|-------------|
|
|
153
|
+
| `search_contacts` | Search personal contacts by name or email |
|
|
154
|
+
| `get_contact` | Get full details of a contact by ID |
|
|
155
|
+
| `create_contact` | Create a new personal contact |
|
|
156
|
+
| `update_contact` | Update an existing contact |
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
## ⚙️ Enterprise / Advanced Configuration
|
|
161
|
+
|
|
162
|
+
### Environment variables
|
|
163
|
+
|
|
164
|
+
| Variable | Description |
|
|
165
|
+
|----------|-------------|
|
|
166
|
+
| `M365_MCP_CLIENT_ID` | Override the built-in app registration with your own Azure AD client ID |
|
|
167
|
+
| `M365_MCP_TENANT_ID` | Restrict authentication to a specific tenant (defaults to `common`) |
|
|
168
|
+
|
|
169
|
+
### Config file override
|
|
170
|
+
|
|
171
|
+
Create `~/.config/m365-mcp/config.json` to set options persistently:
|
|
172
|
+
|
|
173
|
+
```json
|
|
174
|
+
{
|
|
175
|
+
"clientId": "your-azure-ad-app-client-id",
|
|
176
|
+
"tenantId": "your-tenant-id"
|
|
177
|
+
}
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Environment variables take precedence over the config file.
|
|
181
|
+
|
|
182
|
+
### Enterprise consent
|
|
183
|
+
|
|
184
|
+
The built-in app registration is multi-tenant and supports personal Microsoft accounts. First-time sign-in for work/school accounts will show an **"unverified publisher"** consent screen — this is expected for community-distributed apps. An Azure AD admin can pre-consent on behalf of the organization to suppress this prompt for all users. If your organization requires a verified app registration, use the `M365_MCP_CLIENT_ID` override with your own registered application.
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
## 🤝 Contributing
|
|
189
|
+
|
|
190
|
+
Contributions are welcome. See [CONTRIBUTING.md](./CONTRIBUTING.md) for development setup, branching conventions, and the pull request process.
|
|
191
|
+
|
|
192
|
+
---
|
|
193
|
+
|
|
194
|
+
## 📄 License
|
|
195
|
+
|
|
196
|
+
MIT — see [LICENSE](./LICENSE).
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
// src/auth/msal-client.ts
|
|
2
|
+
import fs2 from "fs";
|
|
3
|
+
import path2 from "path";
|
|
4
|
+
import os2 from "os";
|
|
5
|
+
import { PublicClientApplication } from "@azure/msal-node";
|
|
6
|
+
|
|
7
|
+
// src/auth/token-cache.ts
|
|
8
|
+
import fs from "fs";
|
|
9
|
+
import path from "path";
|
|
10
|
+
import os from "os";
|
|
11
|
+
var CACHE_DIR = path.join(os.homedir(), ".config", "m365-mcp");
|
|
12
|
+
var CACHE_PATH = path.join(CACHE_DIR, "token-cache.json");
|
|
13
|
+
function ensureCacheDir() {
|
|
14
|
+
if (!fs.existsSync(CACHE_DIR)) {
|
|
15
|
+
fs.mkdirSync(CACHE_DIR, { recursive: true });
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
var tokenCachePlugin = {
|
|
19
|
+
async beforeCacheAccess(tokenCacheContext) {
|
|
20
|
+
try {
|
|
21
|
+
if (fs.existsSync(CACHE_PATH)) {
|
|
22
|
+
const data = fs.readFileSync(CACHE_PATH, "utf-8");
|
|
23
|
+
tokenCacheContext.tokenCache.deserialize(data);
|
|
24
|
+
}
|
|
25
|
+
} catch (err) {
|
|
26
|
+
process.stderr.write(`[m365-mcp] token cache read error (starting fresh): ${String(err)}
|
|
27
|
+
`);
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
async afterCacheAccess(tokenCacheContext) {
|
|
31
|
+
if (!tokenCacheContext.cacheHasChanged) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
ensureCacheDir();
|
|
36
|
+
const data = tokenCacheContext.tokenCache.serialize();
|
|
37
|
+
fs.writeFileSync(CACHE_PATH, data, { encoding: "utf-8", mode: 384 });
|
|
38
|
+
fs.chmodSync(CACHE_PATH, 384);
|
|
39
|
+
} catch (err) {
|
|
40
|
+
process.stderr.write(`[m365-mcp] token cache write error: ${String(err)}
|
|
41
|
+
`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// src/auth/app-config.ts
|
|
47
|
+
var DEFAULT_CLIENT_ID = "dc9f42bd-30a5-4c43-a498-305ef0c3ad87";
|
|
48
|
+
|
|
49
|
+
// src/auth/msal-client.ts
|
|
50
|
+
var USER_CONFIG_PATH = path2.join(os2.homedir(), ".config", "m365-mcp", "config.json");
|
|
51
|
+
var LOCAL_CONFIG_PATH = path2.join(process.cwd(), ".m365-mcp.json");
|
|
52
|
+
function readConfigFile(filePath) {
|
|
53
|
+
try {
|
|
54
|
+
if (fs2.existsSync(filePath)) {
|
|
55
|
+
const raw = fs2.readFileSync(filePath, "utf-8");
|
|
56
|
+
return JSON.parse(raw);
|
|
57
|
+
}
|
|
58
|
+
} catch {
|
|
59
|
+
}
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
function loadClientId() {
|
|
63
|
+
const envClientId = process.env["M365_MCP_CLIENT_ID"];
|
|
64
|
+
if (envClientId) {
|
|
65
|
+
return envClientId;
|
|
66
|
+
}
|
|
67
|
+
const userConfig = readConfigFile(USER_CONFIG_PATH);
|
|
68
|
+
if (userConfig?.clientId) {
|
|
69
|
+
return userConfig.clientId;
|
|
70
|
+
}
|
|
71
|
+
const localConfig = readConfigFile(LOCAL_CONFIG_PATH);
|
|
72
|
+
if (localConfig?.clientId) {
|
|
73
|
+
return localConfig.clientId;
|
|
74
|
+
}
|
|
75
|
+
if (DEFAULT_CLIENT_ID) {
|
|
76
|
+
return DEFAULT_CLIENT_ID;
|
|
77
|
+
}
|
|
78
|
+
throw new Error('No client ID configured. Run "npx m365-mcp setup" to get started.');
|
|
79
|
+
}
|
|
80
|
+
function loadTenantId() {
|
|
81
|
+
const envTenantId = process.env["M365_MCP_TENANT_ID"];
|
|
82
|
+
if (envTenantId) {
|
|
83
|
+
return envTenantId;
|
|
84
|
+
}
|
|
85
|
+
const userConfig = readConfigFile(USER_CONFIG_PATH);
|
|
86
|
+
if (userConfig?.tenantId) {
|
|
87
|
+
return userConfig.tenantId;
|
|
88
|
+
}
|
|
89
|
+
const localConfig = readConfigFile(LOCAL_CONFIG_PATH);
|
|
90
|
+
if (localConfig?.tenantId) {
|
|
91
|
+
return localConfig.tenantId;
|
|
92
|
+
}
|
|
93
|
+
return "common";
|
|
94
|
+
}
|
|
95
|
+
var _msalClient;
|
|
96
|
+
function getMsalClient() {
|
|
97
|
+
if (_msalClient) {
|
|
98
|
+
return _msalClient;
|
|
99
|
+
}
|
|
100
|
+
const clientId = loadClientId();
|
|
101
|
+
const tenantId = loadTenantId();
|
|
102
|
+
const msalConfig = {
|
|
103
|
+
auth: {
|
|
104
|
+
clientId,
|
|
105
|
+
authority: `https://login.microsoftonline.com/${tenantId}`
|
|
106
|
+
},
|
|
107
|
+
cache: {
|
|
108
|
+
cachePlugin: tokenCachePlugin
|
|
109
|
+
},
|
|
110
|
+
system: {
|
|
111
|
+
loggerOptions: {
|
|
112
|
+
loggerCallback: (_level, message, containsPii) => {
|
|
113
|
+
if (!containsPii) {
|
|
114
|
+
process.stderr.write(`[msal] ${message}
|
|
115
|
+
`);
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
piiLoggingEnabled: false
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
_msalClient = new PublicClientApplication(msalConfig);
|
|
123
|
+
return _msalClient;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// src/auth/scopes.ts
|
|
127
|
+
var GRAPH_SCOPES = [
|
|
128
|
+
"Mail.ReadWrite",
|
|
129
|
+
"Mail.Send",
|
|
130
|
+
"Calendars.ReadWrite",
|
|
131
|
+
"Tasks.ReadWrite",
|
|
132
|
+
"Contacts.ReadWrite",
|
|
133
|
+
"offline_access",
|
|
134
|
+
"User.Read"
|
|
135
|
+
];
|
|
136
|
+
|
|
137
|
+
// src/auth/device-code-flow.ts
|
|
138
|
+
async function trySilentAcquire() {
|
|
139
|
+
const client = getMsalClient();
|
|
140
|
+
const accounts = await client.getAllAccounts();
|
|
141
|
+
if (accounts.length === 0) {
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
for (const account of accounts) {
|
|
145
|
+
try {
|
|
146
|
+
const result = await client.acquireTokenSilent({
|
|
147
|
+
account,
|
|
148
|
+
scopes: GRAPH_SCOPES
|
|
149
|
+
});
|
|
150
|
+
if (result?.accessToken) {
|
|
151
|
+
return result.accessToken;
|
|
152
|
+
}
|
|
153
|
+
} catch {
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
async function acquireToken() {
|
|
159
|
+
const silentToken = await trySilentAcquire();
|
|
160
|
+
if (silentToken) {
|
|
161
|
+
return silentToken;
|
|
162
|
+
}
|
|
163
|
+
const client = getMsalClient();
|
|
164
|
+
const deviceCodeRequest = {
|
|
165
|
+
scopes: GRAPH_SCOPES,
|
|
166
|
+
deviceCodeCallback: (response) => {
|
|
167
|
+
process.stderr.write("\n[m365-mcp] Authentication required\n");
|
|
168
|
+
process.stderr.write(`[m365-mcp] ${response.message}
|
|
169
|
+
|
|
170
|
+
`);
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
const result = await client.acquireTokenByDeviceCode(deviceCodeRequest);
|
|
174
|
+
if (!result?.accessToken) {
|
|
175
|
+
throw new Error("Device code authentication did not return an access token.");
|
|
176
|
+
}
|
|
177
|
+
return result.accessToken;
|
|
178
|
+
}
|
|
179
|
+
async function runAuthCommand() {
|
|
180
|
+
process.stderr.write("[m365-mcp] Starting authentication...\n");
|
|
181
|
+
try {
|
|
182
|
+
const token = await acquireToken();
|
|
183
|
+
if (!token) {
|
|
184
|
+
process.stderr.write("[m365-mcp] Authentication failed \u2014 no token returned.\n");
|
|
185
|
+
process.exit(1);
|
|
186
|
+
}
|
|
187
|
+
const client = getMsalClient();
|
|
188
|
+
const accounts = await client.getAllAccounts();
|
|
189
|
+
const account = accounts[0];
|
|
190
|
+
if (account) {
|
|
191
|
+
process.stderr.write(`[m365-mcp] Authenticated as: ${account.username}
|
|
192
|
+
`);
|
|
193
|
+
} else {
|
|
194
|
+
process.stderr.write("[m365-mcp] Authentication successful.\n");
|
|
195
|
+
}
|
|
196
|
+
} catch (err) {
|
|
197
|
+
process.stderr.write(`[m365-mcp] Authentication error: ${String(err)}
|
|
198
|
+
`);
|
|
199
|
+
process.exit(1);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export {
|
|
204
|
+
acquireToken,
|
|
205
|
+
runAuthCommand
|
|
206
|
+
};
|
|
207
|
+
//# sourceMappingURL=chunk-JEMHJMEL.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/auth/msal-client.ts","../src/auth/token-cache.ts","../src/auth/app-config.ts","../src/auth/scopes.ts","../src/auth/device-code-flow.ts"],"sourcesContent":["import fs from 'fs';\nimport path from 'path';\nimport os from 'os';\nimport { PublicClientApplication } from '@azure/msal-node';\nimport type { Configuration } from '@azure/msal-node';\nimport { tokenCachePlugin } from './token-cache.js';\nimport { DEFAULT_CLIENT_ID } from './app-config.js';\n\n// Priority 2: ~/.config/m365-mcp/config.json (written by `setup` command)\nconst USER_CONFIG_PATH = path.join(os.homedir(), '.config', 'm365-mcp', 'config.json');\n// Priority 3: legacy per-directory config\nconst LOCAL_CONFIG_PATH = path.join(process.cwd(), '.m365-mcp.json');\n\ninterface M365Config {\n clientId: string;\n tenantId?: string;\n}\n\nfunction readConfigFile(filePath: string): M365Config | null {\n try {\n if (fs.existsSync(filePath)) {\n const raw = fs.readFileSync(filePath, 'utf-8');\n return JSON.parse(raw) as M365Config;\n }\n } catch {\n // unreadable or malformed — treat as missing\n }\n return null;\n}\n\nfunction loadClientId(): string {\n // 1. Environment variable\n const envClientId = process.env['M365_MCP_CLIENT_ID'];\n if (envClientId) {\n return envClientId;\n }\n\n // 2. ~/.config/m365-mcp/config.json (written by `setup` command)\n const userConfig = readConfigFile(USER_CONFIG_PATH);\n if (userConfig?.clientId) {\n return userConfig.clientId;\n }\n\n // 3. .m365-mcp.json in cwd (legacy fallback)\n const localConfig = readConfigFile(LOCAL_CONFIG_PATH);\n if (localConfig?.clientId) {\n return localConfig.clientId;\n }\n\n // 4. Shared publisher-registered app ID baked into the package\n if (DEFAULT_CLIENT_ID) {\n return DEFAULT_CLIENT_ID;\n }\n\n throw new Error('No client ID configured. Run \"npx m365-mcp setup\" to get started.');\n}\n\nfunction loadTenantId(): string {\n const envTenantId = process.env['M365_MCP_TENANT_ID'];\n if (envTenantId) {\n return envTenantId;\n }\n\n // 2. ~/.config/m365-mcp/config.json\n const userConfig = readConfigFile(USER_CONFIG_PATH);\n if (userConfig?.tenantId) {\n return userConfig.tenantId;\n }\n\n // 3. .m365-mcp.json in cwd (legacy fallback)\n const localConfig = readConfigFile(LOCAL_CONFIG_PATH);\n if (localConfig?.tenantId) {\n return localConfig.tenantId;\n }\n\n // Use common endpoint for multi-tenant / personal accounts\n return 'common';\n}\n\nlet _msalClient: PublicClientApplication | undefined;\n\nexport function getMsalClient(): PublicClientApplication {\n if (_msalClient) {\n return _msalClient;\n }\n\n const clientId = loadClientId();\n const tenantId = loadTenantId();\n\n const msalConfig: Configuration = {\n auth: {\n clientId,\n authority: `https://login.microsoftonline.com/${tenantId}`,\n },\n cache: {\n cachePlugin: tokenCachePlugin,\n },\n system: {\n loggerOptions: {\n loggerCallback: (_level, message, containsPii) => {\n if (!containsPii) {\n process.stderr.write(`[msal] ${message}\\n`);\n }\n },\n piiLoggingEnabled: false,\n },\n },\n };\n\n _msalClient = new PublicClientApplication(msalConfig);\n return _msalClient;\n}\n","import fs from 'fs';\nimport path from 'path';\nimport os from 'os';\nimport type { ICachePlugin, TokenCacheContext } from '@azure/msal-node';\n\nconst CACHE_DIR = path.join(os.homedir(), '.config', 'm365-mcp');\nconst CACHE_PATH = path.join(CACHE_DIR, 'token-cache.json');\n\nfunction ensureCacheDir(): void {\n if (!fs.existsSync(CACHE_DIR)) {\n fs.mkdirSync(CACHE_DIR, { recursive: true });\n }\n}\n\nexport const tokenCachePlugin: ICachePlugin = {\n async beforeCacheAccess(tokenCacheContext: TokenCacheContext): Promise<void> {\n try {\n if (fs.existsSync(CACHE_PATH)) {\n const data = fs.readFileSync(CACHE_PATH, 'utf-8');\n tokenCacheContext.tokenCache.deserialize(data);\n }\n } catch (err) {\n // Cache file unreadable or corrupt — start fresh\n process.stderr.write(`[m365-mcp] token cache read error (starting fresh): ${String(err)}\\n`);\n }\n },\n\n async afterCacheAccess(tokenCacheContext: TokenCacheContext): Promise<void> {\n if (!tokenCacheContext.cacheHasChanged) {\n return;\n }\n try {\n ensureCacheDir();\n const data = tokenCacheContext.tokenCache.serialize();\n fs.writeFileSync(CACHE_PATH, data, { encoding: 'utf-8', mode: 0o600 });\n // Enforce 600 even if file already existed with different permissions\n fs.chmodSync(CACHE_PATH, 0o600);\n } catch (err) {\n process.stderr.write(`[m365-mcp] token cache write error: ${String(err)}\\n`);\n }\n },\n};\n\nexport { CACHE_PATH };\n","// Shared public client app registration for m365-mcp.\n// Registered once by the publisher; all users share this client ID.\n// Override via M365_MCP_CLIENT_ID env var or ~/.config/m365-mcp/config.json for enterprise use.\nexport const DEFAULT_CLIENT_ID = 'dc9f42bd-30a5-4c43-a498-305ef0c3ad87';\n","/**\n * Microsoft Graph API scopes required by this server.\n * offline_access is required for refresh token acquisition.\n */\nexport const GRAPH_SCOPES: string[] = [\n 'Mail.ReadWrite',\n 'Mail.Send',\n 'Calendars.ReadWrite',\n 'Tasks.ReadWrite',\n 'Contacts.ReadWrite',\n 'offline_access',\n 'User.Read',\n];\n","import type { AccountInfo, AuthenticationResult, DeviceCodeRequest } from '@azure/msal-node';\nimport { getMsalClient } from './msal-client.js';\nimport { GRAPH_SCOPES } from './scopes.js';\n\n/**\n * Attempts silent token acquisition using cached accounts.\n * Returns null if no cached accounts exist or silent acquisition fails.\n */\nasync function trySilentAcquire(): Promise<string | null> {\n const client = getMsalClient();\n const accounts = await client.getAllAccounts();\n\n if (accounts.length === 0) {\n return null;\n }\n\n // Try each cached account — use the first one that succeeds\n for (const account of accounts) {\n try {\n const result: AuthenticationResult | null = await client.acquireTokenSilent({\n account,\n scopes: GRAPH_SCOPES,\n });\n if (result?.accessToken) {\n return result.accessToken;\n }\n } catch {\n // Silent acquisition failed for this account — try next or fall through to device code\n }\n }\n\n return null;\n}\n\n/**\n * Acquires an access token. Tries silent auth first; falls back to device code flow.\n * Prints device code instructions to stderr (stdout is reserved for MCP JSON-RPC).\n */\nexport async function acquireToken(): Promise<string> {\n const silentToken = await trySilentAcquire();\n if (silentToken) {\n return silentToken;\n }\n\n const client = getMsalClient();\n\n const deviceCodeRequest: DeviceCodeRequest = {\n scopes: GRAPH_SCOPES,\n deviceCodeCallback: (response) => {\n process.stderr.write('\\n[m365-mcp] Authentication required\\n');\n process.stderr.write(`[m365-mcp] ${response.message}\\n\\n`);\n },\n };\n\n const result: AuthenticationResult | null = await client.acquireTokenByDeviceCode(deviceCodeRequest);\n\n if (!result?.accessToken) {\n throw new Error('Device code authentication did not return an access token.');\n }\n\n return result.accessToken;\n}\n\n/**\n * CLI `auth` subcommand — runs device code flow interactively and reports the signed-in account.\n */\nexport async function runAuthCommand(): Promise<void> {\n process.stderr.write('[m365-mcp] Starting authentication...\\n');\n\n try {\n const token = await acquireToken();\n\n if (!token) {\n process.stderr.write('[m365-mcp] Authentication failed — no token returned.\\n');\n process.exit(1);\n }\n\n // Retrieve account info for confirmation message\n const client = getMsalClient();\n const accounts: AccountInfo[] = await client.getAllAccounts();\n const account = accounts[0];\n\n if (account) {\n process.stderr.write(`[m365-mcp] Authenticated as: ${account.username}\\n`);\n } else {\n process.stderr.write('[m365-mcp] Authentication successful.\\n');\n }\n } catch (err) {\n process.stderr.write(`[m365-mcp] Authentication error: ${String(err)}\\n`);\n process.exit(1);\n }\n}\n"],"mappings":"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","names":["fs","path","os","path","os","fs"]}
|