@frustrated/ms-graph-mcp 0.1.4 → 0.1.5
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/settings.local.json +8 -0
- package/README.md +16 -73
- package/package.json +1 -1
- package/src/auth.ts +75 -41
- package/src/config.ts +11 -9
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
<b>Seamless Microsoft Graph Integration for AI Agents.</b>
|
|
9
9
|
</p>
|
|
10
10
|
|
|
11
|
-
A
|
|
11
|
+
A TypeScript MCP (Model Context Protocol) package for personal Microsoft Graph access via CLI. This package enables AI agents to interact with personal Microsoft Graph APIs (Outlook, Calendar, etc.) through a local CLI interface.
|
|
12
12
|
|
|
13
13
|
## Overview
|
|
14
14
|
|
|
@@ -17,7 +17,7 @@ A JSR-based TypeScript MCP (Model Context Protocol) package for personal Microso
|
|
|
17
17
|
- **Local-First:** Prioritizes local execution and user data control.
|
|
18
18
|
- **Multi-Tenant Support:** Works with both personal Microsoft accounts and enterprise tenants.
|
|
19
19
|
- **Secure Authentication:** Implements OAuth 2.0 Authorization Code Flow with PKCE.
|
|
20
|
-
- **Secure Token Storage:**
|
|
20
|
+
- **Secure Token Storage:** Stores the MSAL token cache at `~/.config/ms-graph-mcp/msal_cache.json` with restricted file permissions (`0600`).
|
|
21
21
|
- **User Control:** Provides CLI commands for permission management and revocation.
|
|
22
22
|
- **MCP Standard:** Adheres to the Model Context Protocol for broad agent compatibility.
|
|
23
23
|
|
|
@@ -29,59 +29,30 @@ This package is designed to be integrated as a connection within the Manus UI, a
|
|
|
29
29
|
|
|
30
30
|
Before using the MCP CLI, you need to initialize it once to authenticate with your Microsoft account. This process will guide you through granting necessary permissions.
|
|
31
31
|
|
|
32
|
-
#### Using Bun (`bunx`)
|
|
33
|
-
|
|
34
|
-
```bash
|
|
35
|
-
bunx jsr:@frustrated/ms-graph-mcp init
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
#### Using Deno (`deno run`)
|
|
39
|
-
|
|
40
|
-
```bash
|
|
41
|
-
deno run -A jsr:@frustrated/ms-graph-mcp init
|
|
42
|
-
```
|
|
43
|
-
|
|
44
|
-
#### Using Node.js (`npx`)
|
|
45
|
-
|
|
46
32
|
```bash
|
|
47
|
-
|
|
33
|
+
bunx --bun github:usually-frustrated/ms-graph-mcp init
|
|
48
34
|
```
|
|
49
35
|
|
|
50
|
-
|
|
51
|
-
1.
|
|
52
|
-
2.
|
|
53
|
-
3.
|
|
54
|
-
4.
|
|
36
|
+
This will:
|
|
37
|
+
1. Start a local HTTP server to receive the OAuth callback.
|
|
38
|
+
2. Print an authentication URL — open it in your browser to sign in.
|
|
39
|
+
3. Grant consent for the requested scopes.
|
|
40
|
+
4. Save the MSAL token cache to `~/.config/ms-graph-mcp/msal_cache.json`.
|
|
55
41
|
|
|
56
42
|
### Running the MCP Server
|
|
57
43
|
|
|
58
44
|
Once initialized, Manus agents will typically run the MCP server to interact with Microsoft Graph. The server listens for JSON requests on `stdin` and outputs JSON responses to `stdout`.
|
|
59
45
|
|
|
60
|
-
#### Using Bun (`bunx`)
|
|
61
|
-
|
|
62
|
-
```bash
|
|
63
|
-
bunx jsr:@frustrated/ms-graph-mcp run
|
|
64
|
-
```
|
|
65
|
-
|
|
66
|
-
#### Using Deno (`deno run`)
|
|
67
|
-
|
|
68
|
-
```bash
|
|
69
|
-
deno run -A jsr:@frustrated/ms-graph-mcp run
|
|
70
|
-
```
|
|
71
|
-
|
|
72
|
-
#### Using Node.js (`npx`)
|
|
73
|
-
|
|
74
46
|
```bash
|
|
75
|
-
|
|
47
|
+
bunx --bun github:usually-frustrated/ms-graph-mcp run
|
|
76
48
|
```
|
|
77
49
|
|
|
78
50
|
### Top-Level Tools
|
|
79
51
|
|
|
80
|
-
The Microsoft Graph MCP CLI exposes
|
|
52
|
+
The Microsoft Graph MCP CLI exposes the following tools:
|
|
81
53
|
|
|
82
|
-
* **`mail`**: Manage email communications (e.g., list messages
|
|
83
|
-
* **`calendar`**: Organize calendar events (e.g., create events
|
|
84
|
-
* **`onedrive`**: Interact with OneDrive files and folders (e.g., list files, upload files).
|
|
54
|
+
* **`mail`**: Manage email communications (e.g., list messages).
|
|
55
|
+
* **`calendar`**: Organize calendar events (e.g., create events).
|
|
85
56
|
|
|
86
57
|
For detailed information on specific tools and their functionalities, refer to the [Tools Documentation](./docs/tools/README.md).
|
|
87
58
|
|
|
@@ -89,44 +60,16 @@ For detailed information on specific tools and their functionalities, refer to t
|
|
|
89
60
|
|
|
90
61
|
To view the currently configured Client ID, Tenant ID, and enabled/disabled tools:
|
|
91
62
|
|
|
92
|
-
#### Using Bun (`bunx`)
|
|
93
|
-
|
|
94
|
-
```bash
|
|
95
|
-
bunx jsr:@frustrated/ms-graph-mcp permissions
|
|
96
|
-
```
|
|
97
|
-
|
|
98
|
-
#### Using Deno (`deno run`)
|
|
99
|
-
|
|
100
|
-
```bash
|
|
101
|
-
deno run -A jsr:@frustrated/ms-graph-mcp permissions
|
|
102
|
-
```
|
|
103
|
-
|
|
104
|
-
#### Using Node.js (`npx`)
|
|
105
|
-
|
|
106
63
|
```bash
|
|
107
|
-
|
|
64
|
+
bunx --bun github:usually-frustrated/ms-graph-mcp permissions
|
|
108
65
|
```
|
|
109
66
|
|
|
110
67
|
### Revoking Access
|
|
111
68
|
|
|
112
|
-
To revoke
|
|
113
|
-
|
|
114
|
-
#### Using Bun (`bunx`)
|
|
115
|
-
|
|
116
|
-
```bash
|
|
117
|
-
bunx jsr:@frustrated/ms-graph-mcp revoke
|
|
118
|
-
```
|
|
119
|
-
|
|
120
|
-
#### Using Deno (`deno run`)
|
|
121
|
-
|
|
122
|
-
```bash
|
|
123
|
-
deno run -A jsr:@frustrated/ms-graph-mcp revoke
|
|
124
|
-
```
|
|
125
|
-
|
|
126
|
-
#### Using Node.js (`npx`)
|
|
69
|
+
To revoke authentication and clear all stored tokens:
|
|
127
70
|
|
|
128
71
|
```bash
|
|
129
|
-
|
|
72
|
+
bunx --bun github:usually-frustrated/ms-graph-mcp revoke
|
|
130
73
|
```
|
|
131
74
|
|
|
132
75
|
## Documentation
|
|
@@ -137,7 +80,7 @@ npx jsr @frustrated/ms-graph-mcp revoke
|
|
|
137
80
|
|
|
138
81
|
## Security Considerations
|
|
139
82
|
|
|
140
|
-
-
|
|
83
|
+
- The MSAL token cache is stored at `~/.config/ms-graph-mcp/msal_cache.json` with `0600` permissions (owner read/write only).
|
|
141
84
|
- All communication with Microsoft Graph is over HTTPS.
|
|
142
85
|
- Input validation is performed on all incoming MCP requests.
|
|
143
86
|
- Output from Microsoft Graph is sanitized before being passed to AI agents.
|
package/package.json
CHANGED
package/src/auth.ts
CHANGED
|
@@ -1,14 +1,19 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
import {
|
|
2
|
+
PublicClientApplication,
|
|
3
|
+
Configuration,
|
|
4
|
+
LogLevel,
|
|
5
|
+
CryptoProvider,
|
|
6
|
+
} from "@azure/msal-node";
|
|
7
|
+
import { AddressInfo } from "node:net";
|
|
8
|
+
import { promises as fs } from "node:fs";
|
|
9
|
+
import * as path from "node:path";
|
|
10
|
+
import * as os from "node:os";
|
|
11
|
+
import { createServer } from "node:http";
|
|
7
12
|
|
|
8
13
|
const MSAL_CONFIG: Configuration = {
|
|
9
14
|
auth: {
|
|
10
|
-
clientId:
|
|
11
|
-
authority:
|
|
15
|
+
clientId: "0a74e52a-4d5b-4005-8dad-6b7cf45ec5fe", // Replace with actual Client ID
|
|
16
|
+
authority: "https://login.microsoftonline.com/common",
|
|
12
17
|
},
|
|
13
18
|
system: {
|
|
14
19
|
loggerOptions: {
|
|
@@ -21,9 +26,20 @@ const MSAL_CONFIG: Configuration = {
|
|
|
21
26
|
},
|
|
22
27
|
};
|
|
23
28
|
|
|
24
|
-
const REDIRECT_URI_PATH =
|
|
25
|
-
const TOKEN_CACHE_FILE = path.join(
|
|
26
|
-
|
|
29
|
+
const REDIRECT_URI_PATH = "/auth-callback";
|
|
30
|
+
const TOKEN_CACHE_FILE = path.join(
|
|
31
|
+
os.homedir(),
|
|
32
|
+
".config",
|
|
33
|
+
"ms-graph-mcp",
|
|
34
|
+
"msal_cache.json",
|
|
35
|
+
);
|
|
36
|
+
const SCOPES = [
|
|
37
|
+
"User.Read",
|
|
38
|
+
"Mail.ReadWrite",
|
|
39
|
+
"Calendars.ReadWrite",
|
|
40
|
+
"Files.ReadWrite.All",
|
|
41
|
+
"offline_access",
|
|
42
|
+
];
|
|
27
43
|
|
|
28
44
|
const pca = new PublicClientApplication(MSAL_CONFIG);
|
|
29
45
|
|
|
@@ -35,7 +51,7 @@ async function saveTokenCache() {
|
|
|
35
51
|
|
|
36
52
|
async function loadTokenCache(): Promise<boolean> {
|
|
37
53
|
try {
|
|
38
|
-
const data = await fs.readFile(TOKEN_CACHE_FILE,
|
|
54
|
+
const data = await fs.readFile(TOKEN_CACHE_FILE, "utf-8");
|
|
39
55
|
pca.getTokenCache().deserialize(data);
|
|
40
56
|
return true;
|
|
41
57
|
} catch {
|
|
@@ -44,8 +60,10 @@ async function loadTokenCache(): Promise<boolean> {
|
|
|
44
60
|
}
|
|
45
61
|
|
|
46
62
|
export async function initAuth(): Promise<void> {
|
|
47
|
-
console.log(
|
|
48
|
-
console.log(
|
|
63
|
+
console.log("Initiating Microsoft Graph authentication...");
|
|
64
|
+
console.log(
|
|
65
|
+
"Please ensure you have registered a multi-tenant application in Azure AD with the following redirect URI: http://localhost:PORT/auth-callback",
|
|
66
|
+
);
|
|
49
67
|
|
|
50
68
|
const cryptoProvider = new CryptoProvider();
|
|
51
69
|
const pkceCodes = await cryptoProvider.generatePkceCodes();
|
|
@@ -54,7 +72,7 @@ export async function initAuth(): Promise<void> {
|
|
|
54
72
|
scopes: SCOPES,
|
|
55
73
|
redirectUri: `http://localhost:0${REDIRECT_URI_PATH}`,
|
|
56
74
|
codeChallenge: pkceCodes.challenge,
|
|
57
|
-
codeChallengeMethod:
|
|
75
|
+
codeChallengeMethod: "S256" as const,
|
|
58
76
|
};
|
|
59
77
|
|
|
60
78
|
const authCodeUrl = await pca.getAuthCodeUrl(authCodeUrlParameters);
|
|
@@ -63,7 +81,7 @@ export async function initAuth(): Promise<void> {
|
|
|
63
81
|
const server = createServer(async (req, res) => {
|
|
64
82
|
if (req.url?.startsWith(REDIRECT_URI_PATH)) {
|
|
65
83
|
const url = new URL(`http://localhost${req.url}`);
|
|
66
|
-
const code = url.searchParams.get(
|
|
84
|
+
const code = url.searchParams.get("code");
|
|
67
85
|
|
|
68
86
|
if (code) {
|
|
69
87
|
try {
|
|
@@ -77,39 +95,48 @@ export async function initAuth(): Promise<void> {
|
|
|
77
95
|
|
|
78
96
|
if (tokenResult) {
|
|
79
97
|
await saveTokenCache();
|
|
80
|
-
res.writeHead(200, {
|
|
81
|
-
res.end(
|
|
82
|
-
console.log(
|
|
98
|
+
res.writeHead(200, { "Content-Type": "text/plain" });
|
|
99
|
+
res.end("Authentication successful! You can close this window.");
|
|
100
|
+
console.log("Authentication successful. Tokens saved securely.");
|
|
83
101
|
server.close(() => resolve());
|
|
84
102
|
} else {
|
|
85
|
-
throw new Error(
|
|
103
|
+
throw new Error(
|
|
104
|
+
"Authentication failed: no token result received.",
|
|
105
|
+
);
|
|
86
106
|
}
|
|
87
107
|
} catch (error) {
|
|
88
|
-
console.error(
|
|
89
|
-
res.writeHead(500, {
|
|
90
|
-
res.end(
|
|
108
|
+
console.error("Error acquiring token:", error);
|
|
109
|
+
res.writeHead(500, { "Content-Type": "text/plain" });
|
|
110
|
+
res.end("Authentication failed. Check console for details.");
|
|
91
111
|
server.close(() => reject(error));
|
|
92
112
|
}
|
|
93
113
|
} else {
|
|
94
|
-
res.writeHead(400, {
|
|
95
|
-
res.end(
|
|
96
|
-
server.close(() =>
|
|
114
|
+
res.writeHead(400, { "Content-Type": "text/plain" });
|
|
115
|
+
res.end("Authorization code not found in redirect.");
|
|
116
|
+
server.close(() =>
|
|
117
|
+
reject(new Error("Authorization code not found.")),
|
|
118
|
+
);
|
|
97
119
|
}
|
|
98
120
|
} else {
|
|
99
|
-
res.writeHead(404, {
|
|
100
|
-
res.end(
|
|
121
|
+
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
122
|
+
res.end("Not Found");
|
|
101
123
|
}
|
|
102
124
|
});
|
|
103
125
|
|
|
104
126
|
server.listen(0, () => {
|
|
105
127
|
const addr = server.address() as AddressInfo;
|
|
106
128
|
const finalRedirectUri = `http://localhost:${addr.port}${REDIRECT_URI_PATH}`;
|
|
107
|
-
const finalAuthUrl = authCodeUrl.replace(
|
|
108
|
-
|
|
129
|
+
const finalAuthUrl = authCodeUrl.replace(
|
|
130
|
+
`http://localhost:0${REDIRECT_URI_PATH}`,
|
|
131
|
+
finalRedirectUri,
|
|
132
|
+
);
|
|
133
|
+
console.log(
|
|
134
|
+
`\nOpen this URL in your browser to authenticate:\n\n ${finalAuthUrl}\n`,
|
|
135
|
+
);
|
|
109
136
|
});
|
|
110
137
|
|
|
111
|
-
server.on(
|
|
112
|
-
console.error(
|
|
138
|
+
server.on("error", (err) => {
|
|
139
|
+
console.error("Server error:", err);
|
|
113
140
|
reject(err);
|
|
114
141
|
});
|
|
115
142
|
});
|
|
@@ -120,7 +147,7 @@ export async function getAccessToken(): Promise<string> {
|
|
|
120
147
|
|
|
121
148
|
const accounts = await pca.getAllAccounts();
|
|
122
149
|
if (accounts.length === 0) {
|
|
123
|
-
throw new Error(
|
|
150
|
+
throw new Error("No account found. Please run `ms-graph-mcp init` first.");
|
|
124
151
|
}
|
|
125
152
|
|
|
126
153
|
try {
|
|
@@ -130,36 +157,43 @@ export async function getAccessToken(): Promise<string> {
|
|
|
130
157
|
});
|
|
131
158
|
|
|
132
159
|
if (!tokenResult) {
|
|
133
|
-
throw new Error(
|
|
160
|
+
throw new Error("Failed to acquire access token silently.");
|
|
134
161
|
}
|
|
135
162
|
|
|
136
163
|
await saveTokenCache();
|
|
137
164
|
return tokenResult.accessToken;
|
|
138
165
|
} catch (error) {
|
|
139
|
-
console.error(
|
|
166
|
+
console.error("Error refreshing token:", error);
|
|
140
167
|
await fs.unlink(TOKEN_CACHE_FILE).catch(() => {});
|
|
141
|
-
throw new Error(
|
|
168
|
+
throw new Error(
|
|
169
|
+
"Failed to refresh access token. Please re-authenticate using `ms-graph-mcp init`.",
|
|
170
|
+
);
|
|
142
171
|
}
|
|
143
172
|
}
|
|
144
173
|
|
|
145
174
|
export async function revokeAuth(): Promise<void> {
|
|
146
175
|
try {
|
|
147
176
|
await fs.unlink(TOKEN_CACHE_FILE).catch(() => {});
|
|
148
|
-
console.log(
|
|
177
|
+
console.log("Authentication revoked. All tokens cleared.");
|
|
149
178
|
} catch (error) {
|
|
150
|
-
console.error(
|
|
179
|
+
console.error("Error revoking authentication:", error);
|
|
151
180
|
throw error;
|
|
152
181
|
}
|
|
153
182
|
}
|
|
154
183
|
|
|
155
|
-
export async function getAuthStatus(): Promise<{
|
|
184
|
+
export async function getAuthStatus(): Promise<{
|
|
185
|
+
clientId: string;
|
|
186
|
+
tenantId: string;
|
|
187
|
+
isAuthenticated: boolean;
|
|
188
|
+
}> {
|
|
156
189
|
await loadTokenCache();
|
|
157
190
|
const accounts = await pca.getAllAccounts();
|
|
158
191
|
const isAuthenticated = accounts.length > 0;
|
|
159
|
-
const authority =
|
|
192
|
+
const authority =
|
|
193
|
+
MSAL_CONFIG.auth.authority ?? "https://login.microsoftonline.com/common";
|
|
160
194
|
return {
|
|
161
195
|
clientId: MSAL_CONFIG.auth.clientId,
|
|
162
|
-
tenantId: authority.split(
|
|
196
|
+
tenantId: authority.split("/").pop() || "common",
|
|
163
197
|
isAuthenticated,
|
|
164
198
|
};
|
|
165
199
|
}
|
package/src/config.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { promises as fs } from
|
|
2
|
-
import * as path from
|
|
3
|
-
import * as os from
|
|
1
|
+
import { promises as fs } from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import * as os from "node:os";
|
|
4
4
|
|
|
5
|
-
const CONFIG_DIR = path.join(os.homedir(),
|
|
6
|
-
const CONFIG_FILE = path.join(CONFIG_DIR,
|
|
5
|
+
const CONFIG_DIR = path.join(os.homedir(), ".ms-graph-mcp");
|
|
6
|
+
const CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
|
|
7
7
|
|
|
8
8
|
interface AppConfig {
|
|
9
9
|
clientId: string;
|
|
@@ -12,18 +12,20 @@ interface AppConfig {
|
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
let appConfig: AppConfig = {
|
|
15
|
-
clientId:
|
|
16
|
-
tenantId:
|
|
15
|
+
clientId: "0a74e52a-4d5b-4005-8dad-6b7cf45ec5fe", // Default centralized client ID
|
|
16
|
+
tenantId: "common",
|
|
17
17
|
enabledTools: [],
|
|
18
18
|
};
|
|
19
19
|
|
|
20
20
|
export async function loadConfig(): Promise<AppConfig> {
|
|
21
21
|
try {
|
|
22
|
-
const configContent = await fs.readFile(CONFIG_FILE,
|
|
22
|
+
const configContent = await fs.readFile(CONFIG_FILE, "utf-8");
|
|
23
23
|
appConfig = { ...appConfig, ...JSON.parse(configContent) };
|
|
24
24
|
} catch (error) {
|
|
25
25
|
// If file doesn't exist or is invalid, use default config
|
|
26
|
-
console.warn(
|
|
26
|
+
console.warn(
|
|
27
|
+
"No existing config found or config file is invalid. Using default configuration.",
|
|
28
|
+
);
|
|
27
29
|
}
|
|
28
30
|
return appConfig;
|
|
29
31
|
}
|