@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.
@@ -0,0 +1,8 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(git add:*)",
5
+ "Bash(git push:*)"
6
+ ]
7
+ }
8
+ }
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 JSR-based 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, OneDrive, Calendar, etc.) through a local CLI interface.
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:** Uses OS-specific credential managers for token protection.
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
- npx jsr @frustrated/ms-graph-mcp init
33
+ bunx --bun github:usually-frustrated/ms-graph-mcp init
48
34
  ```
49
35
 
50
- These commands will:
51
- 1. Prompt you to authenticate with your Microsoft account (personal or organizational).
52
- 2. Open your browser to the Microsoft identity platform.
53
- 3. Grant consent for the requested scopes.
54
- 4. Securely store your refresh token locally using `keytar`.
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
- npx jsr @frustrated/ms-graph-mcp run
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 various top-level tools, each corresponding to a major Microsoft Graph service. AI agents can discover sub-tools within these categories as needed.
52
+ The Microsoft Graph MCP CLI exposes the following tools:
81
53
 
82
- * **`mail`**: Manage email communications (e.g., list messages, send messages).
83
- * **`calendar`**: Organize calendar events (e.g., create events, list 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
- npx jsr @frustrated/ms-graph-mcp permissions
64
+ bunx --bun github:usually-frustrated/ms-graph-mcp permissions
108
65
  ```
109
66
 
110
67
  ### Revoking Access
111
68
 
112
- To revoke the refresh token and disconnect the package from your Microsoft account:
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
- npx jsr @frustrated/ms-graph-mcp revoke
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
- - Refresh tokens are stored using OS-specific credential managers (Keychain on macOS, Credential Manager on Windows, Secret Service on Linux).
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@frustrated/ms-graph-mcp",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "description": "A JSR-based TypeScript MCP package for personal Microsoft Graph access via CLI",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
package/src/auth.ts CHANGED
@@ -1,14 +1,19 @@
1
- import { PublicClientApplication, Configuration, LogLevel, CryptoProvider } from '@azure/msal-node';
2
- import { AddressInfo } from 'node:net';
3
- import { promises as fs } from 'node:fs';
4
- import * as path from 'node:path';
5
- import * as os from 'node:os';
6
- import { createServer } from 'node:http';
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: process.env.MS_GRAPH_CLIENT_ID || 'YOUR_CENTRALIZED_CLIENT_ID_HERE', // Replace with actual Client ID
11
- authority: 'https://login.microsoftonline.com/common',
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 = '/auth-callback';
25
- const TOKEN_CACHE_FILE = path.join(os.homedir(), '.config', 'ms-graph-mcp', 'msal_cache.json');
26
- const SCOPES = ['User.Read', 'Mail.ReadWrite', 'Calendars.ReadWrite', 'Files.ReadWrite.All', 'offline_access'];
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, 'utf-8');
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('Initiating Microsoft Graph authentication...');
48
- console.log('Please ensure you have registered a multi-tenant application in Azure AD with the following redirect URI: http://localhost:PORT/auth-callback');
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: 'S256' as const,
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('code');
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, { 'Content-Type': 'text/plain' });
81
- res.end('Authentication successful! You can close this window.');
82
- console.log('Authentication successful. Tokens saved securely.');
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('Authentication failed: no token result received.');
103
+ throw new Error(
104
+ "Authentication failed: no token result received.",
105
+ );
86
106
  }
87
107
  } catch (error) {
88
- console.error('Error acquiring token:', error);
89
- res.writeHead(500, { 'Content-Type': 'text/plain' });
90
- res.end('Authentication failed. Check console for details.');
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, { 'Content-Type': 'text/plain' });
95
- res.end('Authorization code not found in redirect.');
96
- server.close(() => reject(new Error('Authorization code not found.')));
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, { 'Content-Type': 'text/plain' });
100
- res.end('Not Found');
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(`http://localhost:0${REDIRECT_URI_PATH}`, finalRedirectUri);
108
- console.log(`\nOpen this URL in your browser to authenticate:\n\n ${finalAuthUrl}\n`);
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('error', (err) => {
112
- console.error('Server error:', err);
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('No account found. Please run `ms-graph-mcp init` first.');
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('Failed to acquire access token silently.');
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('Error refreshing token:', error);
166
+ console.error("Error refreshing token:", error);
140
167
  await fs.unlink(TOKEN_CACHE_FILE).catch(() => {});
141
- throw new Error('Failed to refresh access token. Please re-authenticate using `ms-graph-mcp init`.');
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('Authentication revoked. All tokens cleared.');
177
+ console.log("Authentication revoked. All tokens cleared.");
149
178
  } catch (error) {
150
- console.error('Error revoking authentication:', error);
179
+ console.error("Error revoking authentication:", error);
151
180
  throw error;
152
181
  }
153
182
  }
154
183
 
155
- export async function getAuthStatus(): Promise<{ clientId: string; tenantId: string; isAuthenticated: boolean }> {
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 = MSAL_CONFIG.auth.authority ?? 'https://login.microsoftonline.com/common';
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('/').pop() || 'common',
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 'node:fs';
2
- import * as path from 'node:path';
3
- import * as os from 'node:os';
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(), '.ms-graph-mcp');
6
- const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
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: process.env.MS_GRAPH_CLIENT_ID || 'YOUR_CENTRALIZED_CLIENT_ID_HERE', // Default centralized client ID
16
- tenantId: 'common',
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, 'utf-8');
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('No existing config found or config file is invalid. Using default configuration.');
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
  }