@softeria/ms-365-mcp-server 0.11.0 → 0.11.2
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/README.md +36 -10
- package/dist/auth-tools.js +104 -0
- package/dist/auth.js +149 -6
- package/dist/cli.js +3 -0
- package/dist/graph-client.js +48 -14
- package/dist/index.js +34 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -60,6 +60,29 @@ get-sharepoint-site-drive-by-id, list-sharepoint-site-items, get-sharepoint-site
|
|
|
60
60
|
get-sharepoint-site-list, list-sharepoint-site-list-items, get-sharepoint-site-list-item,
|
|
61
61
|
get-sharepoint-sites-delta</sub>
|
|
62
62
|
|
|
63
|
+
### Work Scopes Issues
|
|
64
|
+
|
|
65
|
+
If you're having issues accessing work/school features (Teams, SharePoint, etc.), you should pass the
|
|
66
|
+
`--force-work-scopes` flag!
|
|
67
|
+
|
|
68
|
+
```json
|
|
69
|
+
{
|
|
70
|
+
"mcpServers": {
|
|
71
|
+
"ms365": {
|
|
72
|
+
"command": "npx",
|
|
73
|
+
"args": [
|
|
74
|
+
"-y",
|
|
75
|
+
"@softeria/ms-365-mcp-server",
|
|
76
|
+
"--force-work-scopes"
|
|
77
|
+
]
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
While the server should attempt to force a re-login when work scopes are needed, passing the flag explicitly is safer
|
|
84
|
+
and ensures proper scope permissions from the start.
|
|
85
|
+
|
|
63
86
|
**User Profile**
|
|
64
87
|
<sub>get-current-user</sub>
|
|
65
88
|
|
|
@@ -146,22 +169,23 @@ MCP clients will automatically handle the OAuth flow when they see the advertise
|
|
|
146
169
|
|
|
147
170
|
##### Setting up Azure AD for OAuth Testing
|
|
148
171
|
|
|
149
|
-
To use OAuth mode with custom Azure credentials (recommended for production), you'll need to set up an Azure AD app
|
|
172
|
+
To use OAuth mode with custom Azure credentials (recommended for production), you'll need to set up an Azure AD app
|
|
173
|
+
registration:
|
|
150
174
|
|
|
151
175
|
1. **Create Azure AD App Registration**:
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
176
|
+
- Go to [Azure Portal](https://portal.azure.com)
|
|
177
|
+
- Navigate to Azure Active Directory → App registrations → New registration
|
|
178
|
+
- Set name: "MS365 MCP Server"
|
|
155
179
|
|
|
156
180
|
2. **Configure Redirect URIs**:
|
|
157
181
|
Add these redirect URIs for testing with MCP Inspector:
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
182
|
+
- `http://localhost:6274/oauth/callback`
|
|
183
|
+
- `http://localhost:6274/oauth/callback/debug`
|
|
184
|
+
- `http://localhost:3000/callback` (optional, for server callback)
|
|
161
185
|
|
|
162
186
|
3. **Get Credentials**:
|
|
163
|
-
|
|
164
|
-
|
|
187
|
+
- Copy the **Application (client) ID** from Overview page
|
|
188
|
+
- Go to Certificates & secrets → New client secret → Copy the secret value
|
|
165
189
|
|
|
166
190
|
4. **Configure Environment Variables**:
|
|
167
191
|
Create a `.env` file in your project root:
|
|
@@ -175,13 +199,15 @@ With these configured, the server will use your custom Azure app instead of the
|
|
|
175
199
|
|
|
176
200
|
#### 3. Bring Your Own Token (BYOT)
|
|
177
201
|
|
|
178
|
-
If you are running ms-365-mcp-server as part of a larger system that manages Microsoft OAuth tokens externally, you can
|
|
202
|
+
If you are running ms-365-mcp-server as part of a larger system that manages Microsoft OAuth tokens externally, you can
|
|
203
|
+
provide an access token directly to this MCP server:
|
|
179
204
|
|
|
180
205
|
```bash
|
|
181
206
|
MS365_MCP_OAUTH_TOKEN=your_oauth_token npx @softeria/ms-365-mcp-server
|
|
182
207
|
```
|
|
183
208
|
|
|
184
209
|
This method:
|
|
210
|
+
|
|
185
211
|
- Bypasses the interactive authentication flows
|
|
186
212
|
- Uses your pre-existing OAuth token for Microsoft Graph API requests
|
|
187
213
|
- Does not handle token refresh (token lifecycle management is your responsibility)
|
package/dist/auth-tools.js
CHANGED
|
@@ -77,4 +77,108 @@ export function registerAuthTools(server, authManager) {
|
|
|
77
77
|
],
|
|
78
78
|
};
|
|
79
79
|
});
|
|
80
|
+
server.tool('list-accounts', {}, async () => {
|
|
81
|
+
try {
|
|
82
|
+
const accounts = await authManager.listAccounts();
|
|
83
|
+
const selectedAccountId = authManager.getSelectedAccountId();
|
|
84
|
+
const result = accounts.map(account => ({
|
|
85
|
+
id: account.homeAccountId,
|
|
86
|
+
username: account.username,
|
|
87
|
+
name: account.name,
|
|
88
|
+
selected: account.homeAccountId === selectedAccountId
|
|
89
|
+
}));
|
|
90
|
+
return {
|
|
91
|
+
content: [
|
|
92
|
+
{
|
|
93
|
+
type: 'text',
|
|
94
|
+
text: JSON.stringify({ accounts: result }),
|
|
95
|
+
},
|
|
96
|
+
],
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
return {
|
|
101
|
+
content: [
|
|
102
|
+
{
|
|
103
|
+
type: 'text',
|
|
104
|
+
text: JSON.stringify({ error: `Failed to list accounts: ${error.message}` }),
|
|
105
|
+
},
|
|
106
|
+
],
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
server.tool('select-account', {
|
|
111
|
+
accountId: z.string().describe('The account ID to select'),
|
|
112
|
+
}, async ({ accountId }) => {
|
|
113
|
+
try {
|
|
114
|
+
const success = await authManager.selectAccount(accountId);
|
|
115
|
+
if (success) {
|
|
116
|
+
return {
|
|
117
|
+
content: [
|
|
118
|
+
{
|
|
119
|
+
type: 'text',
|
|
120
|
+
text: JSON.stringify({ message: `Selected account: ${accountId}` }),
|
|
121
|
+
},
|
|
122
|
+
],
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
return {
|
|
127
|
+
content: [
|
|
128
|
+
{
|
|
129
|
+
type: 'text',
|
|
130
|
+
text: JSON.stringify({ error: `Account not found: ${accountId}` }),
|
|
131
|
+
},
|
|
132
|
+
],
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
catch (error) {
|
|
137
|
+
return {
|
|
138
|
+
content: [
|
|
139
|
+
{
|
|
140
|
+
type: 'text',
|
|
141
|
+
text: JSON.stringify({ error: `Failed to select account: ${error.message}` }),
|
|
142
|
+
},
|
|
143
|
+
],
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
server.tool('remove-account', {
|
|
148
|
+
accountId: z.string().describe('The account ID to remove'),
|
|
149
|
+
}, async ({ accountId }) => {
|
|
150
|
+
try {
|
|
151
|
+
const success = await authManager.removeAccount(accountId);
|
|
152
|
+
if (success) {
|
|
153
|
+
return {
|
|
154
|
+
content: [
|
|
155
|
+
{
|
|
156
|
+
type: 'text',
|
|
157
|
+
text: JSON.stringify({ message: `Removed account: ${accountId}` }),
|
|
158
|
+
},
|
|
159
|
+
],
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
return {
|
|
164
|
+
content: [
|
|
165
|
+
{
|
|
166
|
+
type: 'text',
|
|
167
|
+
text: JSON.stringify({ error: `Account not found: ${accountId}` }),
|
|
168
|
+
},
|
|
169
|
+
],
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
catch (error) {
|
|
174
|
+
return {
|
|
175
|
+
content: [
|
|
176
|
+
{
|
|
177
|
+
type: 'text',
|
|
178
|
+
text: JSON.stringify({ error: `Failed to remove account: ${error.message}` }),
|
|
179
|
+
},
|
|
180
|
+
],
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
});
|
|
80
184
|
}
|
package/dist/auth.js
CHANGED
|
@@ -9,8 +9,10 @@ const endpoints = await import('./endpoints.json', {
|
|
|
9
9
|
});
|
|
10
10
|
const SERVICE_NAME = 'ms-365-mcp-server';
|
|
11
11
|
const TOKEN_CACHE_ACCOUNT = 'msal-token-cache';
|
|
12
|
+
const SELECTED_ACCOUNT_KEY = 'selected-account';
|
|
12
13
|
const FALLBACK_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
13
14
|
const FALLBACK_PATH = path.join(FALLBACK_DIR, '..', '.token-cache.json');
|
|
15
|
+
const SELECTED_ACCOUNT_PATH = path.join(FALLBACK_DIR, '..', '.selected-account.json');
|
|
14
16
|
const DEFAULT_CONFIG = {
|
|
15
17
|
auth: {
|
|
16
18
|
clientId: process.env.MS365_MCP_CLIENT_ID || '084a3e9f-a9f4-43f7-89f9-d229cf97853e',
|
|
@@ -53,6 +55,7 @@ class AuthManager {
|
|
|
53
55
|
this.msalApp = new PublicClientApplication(this.config);
|
|
54
56
|
this.accessToken = null;
|
|
55
57
|
this.tokenExpiry = null;
|
|
58
|
+
this.selectedAccountId = null;
|
|
56
59
|
const oauthTokenFromEnv = process.env.MS365_MCP_OAUTH_TOKEN;
|
|
57
60
|
this.oauthToken = oauthTokenFromEnv ?? null;
|
|
58
61
|
this.isOAuthMode = oauthTokenFromEnv != null;
|
|
@@ -75,11 +78,38 @@ class AuthManager {
|
|
|
75
78
|
if (cacheData) {
|
|
76
79
|
this.msalApp.getTokenCache().deserialize(cacheData);
|
|
77
80
|
}
|
|
81
|
+
// Load selected account
|
|
82
|
+
await this.loadSelectedAccount();
|
|
78
83
|
}
|
|
79
84
|
catch (error) {
|
|
80
85
|
logger.error(`Error loading token cache: ${error.message}`);
|
|
81
86
|
}
|
|
82
87
|
}
|
|
88
|
+
async loadSelectedAccount() {
|
|
89
|
+
try {
|
|
90
|
+
let selectedAccountData;
|
|
91
|
+
try {
|
|
92
|
+
const cachedData = await keytar.getPassword(SERVICE_NAME, SELECTED_ACCOUNT_KEY);
|
|
93
|
+
if (cachedData) {
|
|
94
|
+
selectedAccountData = cachedData;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
catch (keytarError) {
|
|
98
|
+
logger.warn(`Keychain access failed for selected account, falling back to file storage: ${keytarError.message}`);
|
|
99
|
+
}
|
|
100
|
+
if (!selectedAccountData && fs.existsSync(SELECTED_ACCOUNT_PATH)) {
|
|
101
|
+
selectedAccountData = fs.readFileSync(SELECTED_ACCOUNT_PATH, 'utf8');
|
|
102
|
+
}
|
|
103
|
+
if (selectedAccountData) {
|
|
104
|
+
const parsed = JSON.parse(selectedAccountData);
|
|
105
|
+
this.selectedAccountId = parsed.accountId;
|
|
106
|
+
logger.info(`Loaded selected account: ${this.selectedAccountId}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
logger.error(`Error loading selected account: ${error.message}`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
83
113
|
async saveTokenCache() {
|
|
84
114
|
try {
|
|
85
115
|
const cacheData = this.msalApp.getTokenCache().serialize();
|
|
@@ -95,6 +125,21 @@ class AuthManager {
|
|
|
95
125
|
logger.error(`Error saving token cache: ${error.message}`);
|
|
96
126
|
}
|
|
97
127
|
}
|
|
128
|
+
async saveSelectedAccount() {
|
|
129
|
+
try {
|
|
130
|
+
const selectedAccountData = JSON.stringify({ accountId: this.selectedAccountId });
|
|
131
|
+
try {
|
|
132
|
+
await keytar.setPassword(SERVICE_NAME, SELECTED_ACCOUNT_KEY, selectedAccountData);
|
|
133
|
+
}
|
|
134
|
+
catch (keytarError) {
|
|
135
|
+
logger.warn(`Keychain save failed for selected account, falling back to file storage: ${keytarError.message}`);
|
|
136
|
+
fs.writeFileSync(SELECTED_ACCOUNT_PATH, selectedAccountData);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
logger.error(`Error saving selected account: ${error.message}`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
98
143
|
async setOAuthToken(token) {
|
|
99
144
|
this.oauthToken = token;
|
|
100
145
|
this.isOAuthMode = true;
|
|
@@ -106,10 +151,10 @@ class AuthManager {
|
|
|
106
151
|
if (this.accessToken && this.tokenExpiry && this.tokenExpiry > Date.now() && !forceRefresh) {
|
|
107
152
|
return this.accessToken;
|
|
108
153
|
}
|
|
109
|
-
const
|
|
110
|
-
if (
|
|
154
|
+
const currentAccount = await this.getCurrentAccount();
|
|
155
|
+
if (currentAccount) {
|
|
111
156
|
const silentRequest = {
|
|
112
|
-
account:
|
|
157
|
+
account: currentAccount,
|
|
113
158
|
scopes: this.scopes,
|
|
114
159
|
};
|
|
115
160
|
try {
|
|
@@ -125,6 +170,22 @@ class AuthManager {
|
|
|
125
170
|
}
|
|
126
171
|
throw new Error('No valid token found');
|
|
127
172
|
}
|
|
173
|
+
async getCurrentAccount() {
|
|
174
|
+
const accounts = await this.msalApp.getTokenCache().getAllAccounts();
|
|
175
|
+
if (accounts.length === 0) {
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
// If a specific account is selected, find it
|
|
179
|
+
if (this.selectedAccountId) {
|
|
180
|
+
const selectedAccount = accounts.find((account) => account.homeAccountId === this.selectedAccountId);
|
|
181
|
+
if (selectedAccount) {
|
|
182
|
+
return selectedAccount;
|
|
183
|
+
}
|
|
184
|
+
logger.warn(`Selected account ${this.selectedAccountId} not found, falling back to first account`);
|
|
185
|
+
}
|
|
186
|
+
// Fall back to first account (backward compatibility)
|
|
187
|
+
return accounts[0];
|
|
188
|
+
}
|
|
128
189
|
async acquireTokenByDeviceCode(hack) {
|
|
129
190
|
const deviceCodeRequest = {
|
|
130
191
|
scopes: this.scopes,
|
|
@@ -147,6 +208,12 @@ class AuthManager {
|
|
|
147
208
|
logger.info('Device code login successful');
|
|
148
209
|
this.accessToken = response?.accessToken || null;
|
|
149
210
|
this.tokenExpiry = response?.expiresOn ? new Date(response.expiresOn).getTime() : null;
|
|
211
|
+
// Set the newly authenticated account as selected if no account is currently selected
|
|
212
|
+
if (!this.selectedAccountId && response?.account) {
|
|
213
|
+
this.selectedAccountId = response.account.homeAccountId;
|
|
214
|
+
await this.saveSelectedAccount();
|
|
215
|
+
logger.info(`Auto-selected new account: ${response.account.username}`);
|
|
216
|
+
}
|
|
150
217
|
await this.saveTokenCache();
|
|
151
218
|
return this.accessToken;
|
|
152
219
|
}
|
|
@@ -218,8 +285,10 @@ class AuthManager {
|
|
|
218
285
|
}
|
|
219
286
|
this.accessToken = null;
|
|
220
287
|
this.tokenExpiry = null;
|
|
288
|
+
this.selectedAccountId = null;
|
|
221
289
|
try {
|
|
222
290
|
await keytar.deletePassword(SERVICE_NAME, TOKEN_CACHE_ACCOUNT);
|
|
291
|
+
await keytar.deletePassword(SERVICE_NAME, SELECTED_ACCOUNT_KEY);
|
|
223
292
|
}
|
|
224
293
|
catch (keytarError) {
|
|
225
294
|
logger.warn(`Keychain deletion failed: ${keytarError.message}`);
|
|
@@ -227,6 +296,9 @@ class AuthManager {
|
|
|
227
296
|
if (fs.existsSync(FALLBACK_PATH)) {
|
|
228
297
|
fs.unlinkSync(FALLBACK_PATH);
|
|
229
298
|
}
|
|
299
|
+
if (fs.existsSync(SELECTED_ACCOUNT_PATH)) {
|
|
300
|
+
fs.unlinkSync(SELECTED_ACCOUNT_PATH);
|
|
301
|
+
}
|
|
230
302
|
return true;
|
|
231
303
|
}
|
|
232
304
|
catch (error) {
|
|
@@ -236,8 +308,8 @@ class AuthManager {
|
|
|
236
308
|
}
|
|
237
309
|
async hasWorkAccountPermissions() {
|
|
238
310
|
try {
|
|
239
|
-
const
|
|
240
|
-
if (
|
|
311
|
+
const currentAccount = await this.getCurrentAccount();
|
|
312
|
+
if (!currentAccount) {
|
|
241
313
|
return false;
|
|
242
314
|
}
|
|
243
315
|
const workScopes = endpoints.default
|
|
@@ -246,7 +318,7 @@ class AuthManager {
|
|
|
246
318
|
try {
|
|
247
319
|
await this.msalApp.acquireTokenSilent({
|
|
248
320
|
scopes: workScopes.slice(0, 1),
|
|
249
|
-
account:
|
|
321
|
+
account: currentAccount,
|
|
250
322
|
});
|
|
251
323
|
return true;
|
|
252
324
|
}
|
|
@@ -287,6 +359,12 @@ class AuthManager {
|
|
|
287
359
|
this.accessToken = response?.accessToken || null;
|
|
288
360
|
this.tokenExpiry = response?.expiresOn ? new Date(response.expiresOn).getTime() : null;
|
|
289
361
|
this.scopes = allScopes;
|
|
362
|
+
// Update selected account if this is a new account
|
|
363
|
+
if (response?.account) {
|
|
364
|
+
this.selectedAccountId = response.account.homeAccountId;
|
|
365
|
+
await this.saveSelectedAccount();
|
|
366
|
+
logger.info(`Updated selected account after scope expansion: ${response.account.username}`);
|
|
367
|
+
}
|
|
290
368
|
await this.saveTokenCache();
|
|
291
369
|
return true;
|
|
292
370
|
}
|
|
@@ -295,6 +373,71 @@ class AuthManager {
|
|
|
295
373
|
return false;
|
|
296
374
|
}
|
|
297
375
|
}
|
|
376
|
+
// Multi-account support methods
|
|
377
|
+
async listAccounts() {
|
|
378
|
+
return await this.msalApp.getTokenCache().getAllAccounts();
|
|
379
|
+
}
|
|
380
|
+
async selectAccount(accountId) {
|
|
381
|
+
const accounts = await this.listAccounts();
|
|
382
|
+
const account = accounts.find((acc) => acc.homeAccountId === accountId);
|
|
383
|
+
if (!account) {
|
|
384
|
+
logger.error(`Account with ID ${accountId} not found`);
|
|
385
|
+
return false;
|
|
386
|
+
}
|
|
387
|
+
this.selectedAccountId = accountId;
|
|
388
|
+
await this.saveSelectedAccount();
|
|
389
|
+
// Clear cached tokens to force refresh with new account
|
|
390
|
+
this.accessToken = null;
|
|
391
|
+
this.tokenExpiry = null;
|
|
392
|
+
logger.info(`Selected account: ${account.username} (${accountId})`);
|
|
393
|
+
return true;
|
|
394
|
+
}
|
|
395
|
+
async getTokenForAccount(accountId) {
|
|
396
|
+
const accounts = await this.listAccounts();
|
|
397
|
+
const account = accounts.find((acc) => acc.homeAccountId === accountId);
|
|
398
|
+
if (!account) {
|
|
399
|
+
throw new Error(`Account with ID ${accountId} not found`);
|
|
400
|
+
}
|
|
401
|
+
const silentRequest = {
|
|
402
|
+
account: account,
|
|
403
|
+
scopes: this.scopes,
|
|
404
|
+
};
|
|
405
|
+
try {
|
|
406
|
+
const response = await this.msalApp.acquireTokenSilent(silentRequest);
|
|
407
|
+
return response.accessToken;
|
|
408
|
+
}
|
|
409
|
+
catch (error) {
|
|
410
|
+
logger.error(`Failed to get token for account ${accountId}: ${error.message}`);
|
|
411
|
+
throw error;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
async removeAccount(accountId) {
|
|
415
|
+
const accounts = await this.listAccounts();
|
|
416
|
+
const account = accounts.find((acc) => acc.homeAccountId === accountId);
|
|
417
|
+
if (!account) {
|
|
418
|
+
logger.error(`Account with ID ${accountId} not found`);
|
|
419
|
+
return false;
|
|
420
|
+
}
|
|
421
|
+
try {
|
|
422
|
+
await this.msalApp.getTokenCache().removeAccount(account);
|
|
423
|
+
// If this was the selected account, clear the selection
|
|
424
|
+
if (this.selectedAccountId === accountId) {
|
|
425
|
+
this.selectedAccountId = null;
|
|
426
|
+
await this.saveSelectedAccount();
|
|
427
|
+
this.accessToken = null;
|
|
428
|
+
this.tokenExpiry = null;
|
|
429
|
+
}
|
|
430
|
+
logger.info(`Removed account: ${account.username} (${accountId})`);
|
|
431
|
+
return true;
|
|
432
|
+
}
|
|
433
|
+
catch (error) {
|
|
434
|
+
logger.error(`Failed to remove account ${accountId}: ${error.message}`);
|
|
435
|
+
return false;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
getSelectedAccountId() {
|
|
439
|
+
return this.selectedAccountId;
|
|
440
|
+
}
|
|
298
441
|
requiresWorkAccountScope(toolName) {
|
|
299
442
|
const endpoint = endpoints.default.find((e) => e.toolName === toolName);
|
|
300
443
|
return endpoint?.requiresWorkAccount === true;
|
package/dist/cli.js
CHANGED
|
@@ -15,6 +15,9 @@ program
|
|
|
15
15
|
.option('--login', 'Login using device code flow')
|
|
16
16
|
.option('--logout', 'Log out and clear saved credentials')
|
|
17
17
|
.option('--verify-login', 'Verify login without starting the server')
|
|
18
|
+
.option('--list-accounts', 'List all cached accounts')
|
|
19
|
+
.option('--select-account <accountId>', 'Select a specific account by ID')
|
|
20
|
+
.option('--remove-account <accountId>', 'Remove a specific account by ID')
|
|
18
21
|
.option('--read-only', 'Start server in read-only mode, disabling write operations')
|
|
19
22
|
.option('--http [port]', 'Use Streamable HTTP transport instead of stdio (optionally specify port, default: 3000)')
|
|
20
23
|
.option('--enable-auth-tools', 'Enable login/logout tools when using HTTP mode (disabled by default in HTTP mode)')
|
package/dist/graph-client.js
CHANGED
|
@@ -11,14 +11,39 @@ class GraphClient {
|
|
|
11
11
|
this.accessToken = accessToken;
|
|
12
12
|
this.refreshToken = refreshToken || null;
|
|
13
13
|
}
|
|
14
|
+
async getCurrentAccountId() {
|
|
15
|
+
const currentAccount = await this.authManager.getCurrentAccount();
|
|
16
|
+
return currentAccount?.homeAccountId || null;
|
|
17
|
+
}
|
|
18
|
+
getAccountSessions(accountId) {
|
|
19
|
+
if (!this.sessions.has(accountId)) {
|
|
20
|
+
this.sessions.set(accountId, new Map());
|
|
21
|
+
}
|
|
22
|
+
return this.sessions.get(accountId);
|
|
23
|
+
}
|
|
24
|
+
async getSessionForFile(filePath) {
|
|
25
|
+
const accountId = await this.getCurrentAccountId();
|
|
26
|
+
if (!accountId)
|
|
27
|
+
return null;
|
|
28
|
+
const accountSessions = this.getAccountSessions(accountId);
|
|
29
|
+
return accountSessions.get(filePath) || null;
|
|
30
|
+
}
|
|
31
|
+
async setSessionForFile(filePath, sessionId) {
|
|
32
|
+
const accountId = await this.getCurrentAccountId();
|
|
33
|
+
if (!accountId)
|
|
34
|
+
return;
|
|
35
|
+
const accountSessions = this.getAccountSessions(accountId);
|
|
36
|
+
accountSessions.set(filePath, sessionId);
|
|
37
|
+
}
|
|
14
38
|
async createSession(filePath) {
|
|
15
39
|
try {
|
|
16
40
|
if (!filePath) {
|
|
17
41
|
logger.error('No file path provided for Excel session');
|
|
18
42
|
return null;
|
|
19
43
|
}
|
|
20
|
-
|
|
21
|
-
|
|
44
|
+
const existingSession = await this.getSessionForFile(filePath);
|
|
45
|
+
if (existingSession) {
|
|
46
|
+
return existingSession;
|
|
22
47
|
}
|
|
23
48
|
logger.info(`Creating new Excel session for file: ${filePath}`);
|
|
24
49
|
const accessToken = await this.authManager.getToken();
|
|
@@ -37,7 +62,7 @@ class GraphClient {
|
|
|
37
62
|
}
|
|
38
63
|
const result = await response.json();
|
|
39
64
|
logger.info(`Session created successfully for file: ${filePath}`);
|
|
40
|
-
this.
|
|
65
|
+
await this.setSessionForFile(filePath, result.id);
|
|
41
66
|
return result.id;
|
|
42
67
|
}
|
|
43
68
|
catch (error) {
|
|
@@ -116,7 +141,7 @@ class GraphClient {
|
|
|
116
141
|
!endpoint.startsWith('/teams') &&
|
|
117
142
|
!endpoint.startsWith('/chats') &&
|
|
118
143
|
!endpoint.startsWith('/planner')) {
|
|
119
|
-
sessionId = this.
|
|
144
|
+
sessionId = await this.getSessionForFile(options.excelFile);
|
|
120
145
|
if (!sessionId) {
|
|
121
146
|
sessionId = await this.createSessionWithToken(options.excelFile, accessToken);
|
|
122
147
|
}
|
|
@@ -166,8 +191,9 @@ class GraphClient {
|
|
|
166
191
|
logger.error('No file path provided for Excel session');
|
|
167
192
|
return null;
|
|
168
193
|
}
|
|
169
|
-
|
|
170
|
-
|
|
194
|
+
const existingSession = await this.getSessionForFile(filePath);
|
|
195
|
+
if (existingSession) {
|
|
196
|
+
return existingSession;
|
|
171
197
|
}
|
|
172
198
|
logger.info(`Creating new Excel session for file: ${filePath}`);
|
|
173
199
|
const response = await fetch(`https://graph.microsoft.com/v1.0/me/drive/root:${filePath}:/workbook/createSession`, {
|
|
@@ -185,7 +211,7 @@ class GraphClient {
|
|
|
185
211
|
}
|
|
186
212
|
const result = await response.json();
|
|
187
213
|
logger.info(`Session created successfully for file: ${filePath}`);
|
|
188
|
-
this.
|
|
214
|
+
await this.setSessionForFile(filePath, result.id);
|
|
189
215
|
return result.id;
|
|
190
216
|
}
|
|
191
217
|
catch (error) {
|
|
@@ -236,7 +262,7 @@ class GraphClient {
|
|
|
236
262
|
!endpoint.startsWith('/chats') &&
|
|
237
263
|
!endpoint.startsWith('/planner') &&
|
|
238
264
|
!endpoint.startsWith('/sites')) {
|
|
239
|
-
sessionId = this.
|
|
265
|
+
sessionId = await this.getSessionForFile(options.excelFile);
|
|
240
266
|
if (!sessionId) {
|
|
241
267
|
sessionId = await this.createSession(options.excelFile);
|
|
242
268
|
}
|
|
@@ -407,7 +433,8 @@ class GraphClient {
|
|
|
407
433
|
}
|
|
408
434
|
}
|
|
409
435
|
async closeSession(filePath) {
|
|
410
|
-
|
|
436
|
+
const sessionId = await this.getSessionForFile(filePath);
|
|
437
|
+
if (!filePath || !sessionId) {
|
|
411
438
|
return {
|
|
412
439
|
content: [
|
|
413
440
|
{
|
|
@@ -417,7 +444,6 @@ class GraphClient {
|
|
|
417
444
|
],
|
|
418
445
|
};
|
|
419
446
|
}
|
|
420
|
-
const sessionId = this.sessions.get(filePath);
|
|
421
447
|
try {
|
|
422
448
|
const accessToken = await this.authManager.getToken();
|
|
423
449
|
const response = await fetch(`https://graph.microsoft.com/v1.0/me/drive/root:${filePath}:/workbook/closeSession`, {
|
|
@@ -429,7 +455,11 @@ class GraphClient {
|
|
|
429
455
|
},
|
|
430
456
|
});
|
|
431
457
|
if (response.ok) {
|
|
432
|
-
this.
|
|
458
|
+
const accountId = await this.getCurrentAccountId();
|
|
459
|
+
if (accountId) {
|
|
460
|
+
const accountSessions = this.getAccountSessions(accountId);
|
|
461
|
+
accountSessions.delete(filePath);
|
|
462
|
+
}
|
|
433
463
|
return {
|
|
434
464
|
content: [
|
|
435
465
|
{
|
|
@@ -458,9 +488,13 @@ class GraphClient {
|
|
|
458
488
|
}
|
|
459
489
|
async closeAllSessions() {
|
|
460
490
|
const results = [];
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
491
|
+
const accountId = await this.getCurrentAccountId();
|
|
492
|
+
if (accountId) {
|
|
493
|
+
const accountSessions = this.getAccountSessions(accountId);
|
|
494
|
+
for (const [filePath] of accountSessions) {
|
|
495
|
+
const result = await this.closeSession(filePath);
|
|
496
|
+
results.push(result);
|
|
497
|
+
}
|
|
464
498
|
}
|
|
465
499
|
return {
|
|
466
500
|
content: [
|
package/dist/index.js
CHANGED
|
@@ -40,6 +40,40 @@ async function main() {
|
|
|
40
40
|
console.log(JSON.stringify({ message: 'Logged out successfully' }));
|
|
41
41
|
process.exit(0);
|
|
42
42
|
}
|
|
43
|
+
if (args.listAccounts) {
|
|
44
|
+
const accounts = await authManager.listAccounts();
|
|
45
|
+
const selectedAccountId = authManager.getSelectedAccountId();
|
|
46
|
+
const result = accounts.map(account => ({
|
|
47
|
+
id: account.homeAccountId,
|
|
48
|
+
username: account.username,
|
|
49
|
+
name: account.name,
|
|
50
|
+
selected: account.homeAccountId === selectedAccountId
|
|
51
|
+
}));
|
|
52
|
+
console.log(JSON.stringify({ accounts: result }));
|
|
53
|
+
process.exit(0);
|
|
54
|
+
}
|
|
55
|
+
if (args.selectAccount) {
|
|
56
|
+
const success = await authManager.selectAccount(args.selectAccount);
|
|
57
|
+
if (success) {
|
|
58
|
+
console.log(JSON.stringify({ message: `Selected account: ${args.selectAccount}` }));
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
console.log(JSON.stringify({ error: `Account not found: ${args.selectAccount}` }));
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
process.exit(0);
|
|
65
|
+
}
|
|
66
|
+
if (args.removeAccount) {
|
|
67
|
+
const success = await authManager.removeAccount(args.removeAccount);
|
|
68
|
+
if (success) {
|
|
69
|
+
console.log(JSON.stringify({ message: `Removed account: ${args.removeAccount}` }));
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
console.log(JSON.stringify({ error: `Account not found: ${args.removeAccount}` }));
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
|
75
|
+
process.exit(0);
|
|
76
|
+
}
|
|
43
77
|
const server = new MicrosoftGraphServer(authManager, args);
|
|
44
78
|
await server.initialize(version);
|
|
45
79
|
await server.start();
|