@softeria/ms-365-mcp-server 0.9.13 → 0.10.1
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/.env.example +24 -0
- package/README.md +29 -0
- package/dist/graph-client.js +166 -0
- package/dist/index.js +1 -0
- package/dist/lib/microsoft-auth.js +75 -0
- package/dist/server.js +194 -7
- package/package.json +3 -1
package/.env.example
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Microsoft 365 OAuth Configuration
|
|
2
|
+
# Create an Azure AD app registration and get these values:
|
|
3
|
+
|
|
4
|
+
# Your Azure AD App Registration Client ID
|
|
5
|
+
MS365_MCP_CLIENT_ID=your-azure-ad-app-client-id-here
|
|
6
|
+
|
|
7
|
+
# Your Azure AD App Registration Client Secret
|
|
8
|
+
MS365_MCP_CLIENT_SECRET=your-azure-ad-app-client-secret-here
|
|
9
|
+
|
|
10
|
+
# Tenant ID - use "common" for multi-tenant or your specific tenant ID
|
|
11
|
+
MS365_MCP_TENANT_ID=common
|
|
12
|
+
|
|
13
|
+
# Instructions:
|
|
14
|
+
# 1. Go to https://portal.azure.com
|
|
15
|
+
# 2. Navigate to Azure Active Directory → App registrations → New registration
|
|
16
|
+
# 3. Set name: "MS365 MCP Server"
|
|
17
|
+
# 4. Add these redirect URIs (for MCP Inspector testing):
|
|
18
|
+
# - http://localhost:6274/oauth/callback
|
|
19
|
+
# - http://localhost:6274/oauth/callback/debug
|
|
20
|
+
# - http://localhost:3000/callback (optional, for server callback)
|
|
21
|
+
# 5. Copy the Client ID from Overview page
|
|
22
|
+
# 6. Go to Certificates & secrets → New client secret → Copy the secret value
|
|
23
|
+
# 7. Replace the values above with your actual credentials
|
|
24
|
+
# 8. Rename this file to .env
|
package/README.md
CHANGED
|
@@ -144,6 +144,35 @@ This mode:
|
|
|
144
144
|
|
|
145
145
|
MCP clients will automatically handle the OAuth flow when they see the advertised capabilities.
|
|
146
146
|
|
|
147
|
+
##### Setting up Azure AD for OAuth Testing
|
|
148
|
+
|
|
149
|
+
To use OAuth mode with custom Azure credentials (recommended for production), you'll need to set up an Azure AD app registration:
|
|
150
|
+
|
|
151
|
+
1. **Create Azure AD App Registration**:
|
|
152
|
+
- Go to [Azure Portal](https://portal.azure.com)
|
|
153
|
+
- Navigate to Azure Active Directory → App registrations → New registration
|
|
154
|
+
- Set name: "MS365 MCP Server"
|
|
155
|
+
|
|
156
|
+
2. **Configure Redirect URIs**:
|
|
157
|
+
Add these redirect URIs for testing with MCP Inspector:
|
|
158
|
+
- `http://localhost:6274/oauth/callback`
|
|
159
|
+
- `http://localhost:6274/oauth/callback/debug`
|
|
160
|
+
- `http://localhost:3000/callback` (optional, for server callback)
|
|
161
|
+
|
|
162
|
+
3. **Get Credentials**:
|
|
163
|
+
- Copy the **Application (client) ID** from Overview page
|
|
164
|
+
- Go to Certificates & secrets → New client secret → Copy the secret value
|
|
165
|
+
|
|
166
|
+
4. **Configure Environment Variables**:
|
|
167
|
+
Create a `.env` file in your project root:
|
|
168
|
+
```env
|
|
169
|
+
MS365_MCP_CLIENT_ID=your-azure-ad-app-client-id-here
|
|
170
|
+
MS365_MCP_CLIENT_SECRET=your-azure-ad-app-client-secret-here
|
|
171
|
+
MS365_MCP_TENANT_ID=common
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
With these configured, the server will use your custom Azure app instead of the built-in one.
|
|
175
|
+
|
|
147
176
|
#### 3. Bring Your Own Token (BYOT)
|
|
148
177
|
|
|
149
178
|
If you are running ms-365-mcp-server as part of a larger system that manages Microsoft OAuth tokens externally, you can provide an access token directly to this MCP server:
|
package/dist/graph-client.js
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
import logger from './logger.js';
|
|
2
|
+
import { refreshAccessToken } from './lib/microsoft-auth.js';
|
|
2
3
|
class GraphClient {
|
|
3
4
|
constructor(authManager) {
|
|
5
|
+
this.accessToken = null;
|
|
6
|
+
this.refreshToken = null;
|
|
4
7
|
this.authManager = authManager;
|
|
5
8
|
this.sessions = new Map();
|
|
6
9
|
}
|
|
10
|
+
setOAuthTokens(accessToken, refreshToken) {
|
|
11
|
+
this.accessToken = accessToken;
|
|
12
|
+
this.refreshToken = refreshToken || null;
|
|
13
|
+
}
|
|
7
14
|
async createSession(filePath) {
|
|
8
15
|
try {
|
|
9
16
|
if (!filePath) {
|
|
@@ -38,7 +45,166 @@ class GraphClient {
|
|
|
38
45
|
return null;
|
|
39
46
|
}
|
|
40
47
|
}
|
|
48
|
+
async makeRequest(endpoint, options = {}) {
|
|
49
|
+
// Use OAuth tokens if available, otherwise fall back to authManager
|
|
50
|
+
let accessToken = options.accessToken || this.accessToken || await this.authManager.getToken();
|
|
51
|
+
let refreshToken = options.refreshToken || this.refreshToken;
|
|
52
|
+
if (!accessToken) {
|
|
53
|
+
throw new Error('No access token available');
|
|
54
|
+
}
|
|
55
|
+
try {
|
|
56
|
+
const response = await this.performRequest(endpoint, accessToken, options);
|
|
57
|
+
if (response.status === 401 && refreshToken) {
|
|
58
|
+
// Token expired, try to refresh
|
|
59
|
+
await this.refreshAccessToken(refreshToken);
|
|
60
|
+
// Update token for retry
|
|
61
|
+
accessToken = this.accessToken || accessToken;
|
|
62
|
+
if (!accessToken) {
|
|
63
|
+
throw new Error('Failed to refresh access token');
|
|
64
|
+
}
|
|
65
|
+
// Retry the request with new token
|
|
66
|
+
return this.performRequest(endpoint, accessToken, options);
|
|
67
|
+
}
|
|
68
|
+
if (!response.ok) {
|
|
69
|
+
throw new Error(`Microsoft Graph API error: ${response.status} ${response.statusText}`);
|
|
70
|
+
}
|
|
71
|
+
return response.json();
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
logger.error('Microsoft Graph API request failed:', error);
|
|
75
|
+
throw error;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
async refreshAccessToken(refreshToken) {
|
|
79
|
+
const tenantId = process.env.MS365_MCP_TENANT_ID || 'common';
|
|
80
|
+
const clientId = process.env.MS365_MCP_CLIENT_ID || '084a3e9f-a9f4-43f7-89f9-d229cf97853e';
|
|
81
|
+
const clientSecret = process.env.MS365_MCP_CLIENT_SECRET;
|
|
82
|
+
if (!clientSecret) {
|
|
83
|
+
throw new Error('MS365_MCP_CLIENT_SECRET not configured');
|
|
84
|
+
}
|
|
85
|
+
const response = await refreshAccessToken(refreshToken, clientId, clientSecret, tenantId);
|
|
86
|
+
this.accessToken = response.access_token;
|
|
87
|
+
if (response.refresh_token) {
|
|
88
|
+
this.refreshToken = response.refresh_token;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
async performRequest(endpoint, accessToken, options) {
|
|
92
|
+
let url;
|
|
93
|
+
let sessionId = null;
|
|
94
|
+
if (options.excelFile &&
|
|
95
|
+
!endpoint.startsWith('/drive') &&
|
|
96
|
+
!endpoint.startsWith('/users') &&
|
|
97
|
+
!endpoint.startsWith('/me') &&
|
|
98
|
+
!endpoint.startsWith('/teams') &&
|
|
99
|
+
!endpoint.startsWith('/chats') &&
|
|
100
|
+
!endpoint.startsWith('/planner')) {
|
|
101
|
+
sessionId = this.sessions.get(options.excelFile) || null;
|
|
102
|
+
if (!sessionId) {
|
|
103
|
+
sessionId = await this.createSessionWithToken(options.excelFile, accessToken);
|
|
104
|
+
}
|
|
105
|
+
url = `https://graph.microsoft.com/v1.0/me/drive/root:${options.excelFile}:${endpoint}`;
|
|
106
|
+
}
|
|
107
|
+
else if (endpoint.startsWith('/drive') ||
|
|
108
|
+
endpoint.startsWith('/users') ||
|
|
109
|
+
endpoint.startsWith('/me') ||
|
|
110
|
+
endpoint.startsWith('/teams') ||
|
|
111
|
+
endpoint.startsWith('/chats') ||
|
|
112
|
+
endpoint.startsWith('/planner')) {
|
|
113
|
+
url = `https://graph.microsoft.com/v1.0${endpoint}`;
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
throw new Error('Excel operation requested without specifying a file');
|
|
117
|
+
}
|
|
118
|
+
const headers = {
|
|
119
|
+
'Authorization': `Bearer ${accessToken}`,
|
|
120
|
+
'Content-Type': 'application/json',
|
|
121
|
+
...(sessionId && { 'workbook-session-id': sessionId }),
|
|
122
|
+
...options.headers,
|
|
123
|
+
};
|
|
124
|
+
return fetch(url, {
|
|
125
|
+
method: options.method || 'GET',
|
|
126
|
+
headers,
|
|
127
|
+
body: options.body,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
41
130
|
async graphRequest(endpoint, options = {}) {
|
|
131
|
+
try {
|
|
132
|
+
logger.info(`Calling ${endpoint} with options: ${JSON.stringify(options)}`);
|
|
133
|
+
// Use new OAuth-aware request method
|
|
134
|
+
const result = await this.makeRequest(endpoint, options);
|
|
135
|
+
return this.formatJsonResponse(result, options.rawResponse);
|
|
136
|
+
}
|
|
137
|
+
catch (error) {
|
|
138
|
+
logger.error(`Error in Graph API request: ${error}`);
|
|
139
|
+
return {
|
|
140
|
+
content: [{ type: 'text', text: JSON.stringify({ error: error.message }) }],
|
|
141
|
+
isError: true,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
async createSessionWithToken(filePath, accessToken) {
|
|
146
|
+
try {
|
|
147
|
+
if (!filePath) {
|
|
148
|
+
logger.error('No file path provided for Excel session');
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
if (this.sessions.has(filePath)) {
|
|
152
|
+
return this.sessions.get(filePath) || null;
|
|
153
|
+
}
|
|
154
|
+
logger.info(`Creating new Excel session for file: ${filePath}`);
|
|
155
|
+
const response = await fetch(`https://graph.microsoft.com/v1.0/me/drive/root:${filePath}:/workbook/createSession`, {
|
|
156
|
+
method: 'POST',
|
|
157
|
+
headers: {
|
|
158
|
+
Authorization: `Bearer ${accessToken}`,
|
|
159
|
+
'Content-Type': 'application/json',
|
|
160
|
+
},
|
|
161
|
+
body: JSON.stringify({ persistChanges: true }),
|
|
162
|
+
});
|
|
163
|
+
if (!response.ok) {
|
|
164
|
+
const errorText = await response.text();
|
|
165
|
+
logger.error(`Failed to create session: ${response.status} - ${errorText}`);
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
const result = await response.json();
|
|
169
|
+
logger.info(`Session created successfully for file: ${filePath}`);
|
|
170
|
+
this.sessions.set(filePath, result.id);
|
|
171
|
+
return result.id;
|
|
172
|
+
}
|
|
173
|
+
catch (error) {
|
|
174
|
+
logger.error(`Error creating Excel session: ${error}`);
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
formatJsonResponse(data, rawResponse = false) {
|
|
179
|
+
if (rawResponse) {
|
|
180
|
+
return {
|
|
181
|
+
content: [{ type: 'text', text: JSON.stringify(data) }],
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
if (data === null || data === undefined) {
|
|
185
|
+
return {
|
|
186
|
+
content: [{ type: 'text', text: JSON.stringify({ success: true }) }],
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
// Remove OData properties
|
|
190
|
+
const removeODataProps = (obj) => {
|
|
191
|
+
if (typeof obj === 'object' && obj !== null) {
|
|
192
|
+
Object.keys(obj).forEach((key) => {
|
|
193
|
+
if (key.startsWith('@odata.')) {
|
|
194
|
+
delete obj[key];
|
|
195
|
+
}
|
|
196
|
+
else if (typeof obj[key] === 'object') {
|
|
197
|
+
removeODataProps(obj[key]);
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
removeODataProps(data);
|
|
203
|
+
return {
|
|
204
|
+
content: [{ type: 'text', text: JSON.stringify(data, null, 2) }],
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
async graphRequestOld(endpoint, options = {}) {
|
|
42
208
|
try {
|
|
43
209
|
logger.info(`Calling ${endpoint} with options: ${JSON.stringify(options)}`);
|
|
44
210
|
let accessToken = await this.authManager.getToken();
|
package/dist/index.js
CHANGED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import logger from '../logger.js';
|
|
2
|
+
/**
|
|
3
|
+
* Microsoft Bearer Token Auth Middleware validates that the request has a valid Microsoft access token
|
|
4
|
+
* The token is passed in the Authorization header as a Bearer token
|
|
5
|
+
*/
|
|
6
|
+
export const microsoftBearerTokenAuthMiddleware = (req, res, next) => {
|
|
7
|
+
const authHeader = req.headers.authorization;
|
|
8
|
+
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
|
9
|
+
res.status(401).json({ error: 'Missing or invalid access token' });
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
const accessToken = authHeader.substring(7);
|
|
13
|
+
// For Microsoft Graph, we don't validate the token here - we'll let the API calls fail if it's invalid
|
|
14
|
+
// and handle token refresh in the GraphClient
|
|
15
|
+
// Extract refresh token from a custom header (if provided)
|
|
16
|
+
const refreshToken = req.headers['x-microsoft-refresh-token'] || '';
|
|
17
|
+
// Store tokens in request for later use
|
|
18
|
+
req.microsoftAuth = {
|
|
19
|
+
accessToken,
|
|
20
|
+
refreshToken,
|
|
21
|
+
};
|
|
22
|
+
next();
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Exchange authorization code for access token
|
|
26
|
+
*/
|
|
27
|
+
export async function exchangeCodeForToken(code, redirectUri, clientId, clientSecret, tenantId = 'common', codeVerifier) {
|
|
28
|
+
const params = new URLSearchParams({
|
|
29
|
+
grant_type: 'authorization_code',
|
|
30
|
+
code,
|
|
31
|
+
redirect_uri: redirectUri,
|
|
32
|
+
client_id: clientId,
|
|
33
|
+
client_secret: clientSecret,
|
|
34
|
+
});
|
|
35
|
+
// Add code_verifier for PKCE flow
|
|
36
|
+
if (codeVerifier) {
|
|
37
|
+
params.append('code_verifier', codeVerifier);
|
|
38
|
+
}
|
|
39
|
+
const response = await fetch(`https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`, {
|
|
40
|
+
method: 'POST',
|
|
41
|
+
headers: {
|
|
42
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
43
|
+
},
|
|
44
|
+
body: params
|
|
45
|
+
});
|
|
46
|
+
if (!response.ok) {
|
|
47
|
+
const error = await response.text();
|
|
48
|
+
logger.error(`Failed to exchange code for token: ${error}`);
|
|
49
|
+
throw new Error(`Failed to exchange code for token: ${error}`);
|
|
50
|
+
}
|
|
51
|
+
return response.json();
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Refresh an access token
|
|
55
|
+
*/
|
|
56
|
+
export async function refreshAccessToken(refreshToken, clientId, clientSecret, tenantId = 'common') {
|
|
57
|
+
const response = await fetch(`https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`, {
|
|
58
|
+
method: 'POST',
|
|
59
|
+
headers: {
|
|
60
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
61
|
+
},
|
|
62
|
+
body: new URLSearchParams({
|
|
63
|
+
grant_type: 'refresh_token',
|
|
64
|
+
refresh_token: refreshToken,
|
|
65
|
+
client_id: clientId,
|
|
66
|
+
client_secret: clientSecret,
|
|
67
|
+
})
|
|
68
|
+
});
|
|
69
|
+
if (!response.ok) {
|
|
70
|
+
const error = await response.text();
|
|
71
|
+
logger.error(`Failed to refresh token: ${error}`);
|
|
72
|
+
throw new Error(`Failed to refresh token: ${error}`);
|
|
73
|
+
}
|
|
74
|
+
return response.json();
|
|
75
|
+
}
|
package/dist/server.js
CHANGED
|
@@ -2,13 +2,15 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
|
2
2
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
3
|
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
4
4
|
import { mcpAuthRouter } from '@modelcontextprotocol/sdk/server/auth/router.js';
|
|
5
|
-
import { requireBearerAuth } from '@modelcontextprotocol/sdk/server/auth/middleware/bearerAuth.js';
|
|
6
5
|
import express from 'express';
|
|
6
|
+
import crypto from 'crypto';
|
|
7
7
|
import logger, { enableConsoleLogging } from './logger.js';
|
|
8
8
|
import { registerAuthTools } from './auth-tools.js';
|
|
9
9
|
import { registerGraphTools } from './graph-tools.js';
|
|
10
10
|
import GraphClient from './graph-client.js';
|
|
11
11
|
import { MicrosoftOAuthProvider } from './oauth-provider.js';
|
|
12
|
+
import { microsoftBearerTokenAuthMiddleware, exchangeCodeForToken, refreshAccessToken } from './lib/microsoft-auth.js';
|
|
13
|
+
const registeredClients = new Map();
|
|
12
14
|
class MicrosoftGraphServer {
|
|
13
15
|
constructor(authManager, options = {}) {
|
|
14
16
|
this.authManager = authManager;
|
|
@@ -32,6 +34,13 @@ class MicrosoftGraphServer {
|
|
|
32
34
|
enableConsoleLogging();
|
|
33
35
|
}
|
|
34
36
|
logger.info('Microsoft 365 MCP Server starting...');
|
|
37
|
+
// Debug: Check if environment variables are loaded
|
|
38
|
+
logger.info('Environment Variables Check:', {
|
|
39
|
+
CLIENT_ID: process.env.MS365_MCP_CLIENT_ID ? `${process.env.MS365_MCP_CLIENT_ID.substring(0, 8)}...` : 'NOT SET',
|
|
40
|
+
CLIENT_SECRET: process.env.MS365_MCP_CLIENT_SECRET ? `${process.env.MS365_MCP_CLIENT_SECRET.substring(0, 8)}...` : 'NOT SET',
|
|
41
|
+
TENANT_ID: process.env.MS365_MCP_TENANT_ID || 'NOT SET',
|
|
42
|
+
NODE_ENV: process.env.NODE_ENV || 'NOT SET'
|
|
43
|
+
});
|
|
35
44
|
if (this.options.readOnly) {
|
|
36
45
|
logger.info('Server running in READ-ONLY mode. Write operations are disabled.');
|
|
37
46
|
}
|
|
@@ -39,18 +48,191 @@ class MicrosoftGraphServer {
|
|
|
39
48
|
const port = typeof this.options.http === 'string' ? parseInt(this.options.http) : 3000;
|
|
40
49
|
const app = express();
|
|
41
50
|
app.use(express.json());
|
|
51
|
+
app.use(express.urlencoded({ extended: true }));
|
|
52
|
+
// Add CORS headers for all routes
|
|
53
|
+
app.use((req, res, next) => {
|
|
54
|
+
res.header('Access-Control-Allow-Origin', '*');
|
|
55
|
+
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
|
56
|
+
res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization, mcp-protocol-version');
|
|
57
|
+
// Handle preflight requests
|
|
58
|
+
if (req.method === 'OPTIONS') {
|
|
59
|
+
res.sendStatus(200);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
next();
|
|
63
|
+
});
|
|
42
64
|
const oauthProvider = new MicrosoftOAuthProvider(this.authManager);
|
|
65
|
+
// OAuth Authorization Server Discovery
|
|
66
|
+
app.get('/.well-known/oauth-authorization-server', async (req, res) => {
|
|
67
|
+
const url = new URL(`${req.protocol}://${req.get('host')}`);
|
|
68
|
+
res.json({
|
|
69
|
+
issuer: url.origin,
|
|
70
|
+
authorization_endpoint: `${url.origin}/authorize`,
|
|
71
|
+
token_endpoint: `${url.origin}/token`,
|
|
72
|
+
registration_endpoint: `${url.origin}/register`,
|
|
73
|
+
response_types_supported: ['code'],
|
|
74
|
+
response_modes_supported: ['query'],
|
|
75
|
+
grant_types_supported: ['authorization_code', 'refresh_token'],
|
|
76
|
+
token_endpoint_auth_methods_supported: ['none'],
|
|
77
|
+
code_challenge_methods_supported: ['S256'],
|
|
78
|
+
scopes_supported: [
|
|
79
|
+
'User.Read', 'Files.Read', 'Mail.Read'
|
|
80
|
+
],
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
// OAuth Protected Resource Discovery
|
|
84
|
+
app.get('/.well-known/oauth-protected-resource', async (req, res) => {
|
|
85
|
+
const url = new URL(`${req.protocol}://${req.get('host')}`);
|
|
86
|
+
res.json({
|
|
87
|
+
resource: `${url.origin}/mcp`,
|
|
88
|
+
authorization_servers: [url.origin],
|
|
89
|
+
scopes_supported: [
|
|
90
|
+
'User.Read', 'Files.Read', 'Mail.Read'
|
|
91
|
+
],
|
|
92
|
+
bearer_methods_supported: ['header'],
|
|
93
|
+
resource_documentation: `${url.origin}`,
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
// Dynamic Client Registration endpoint
|
|
97
|
+
app.post('/register', async (req, res) => {
|
|
98
|
+
const body = req.body;
|
|
99
|
+
// Generate a client ID
|
|
100
|
+
const clientId = crypto.randomUUID();
|
|
101
|
+
// Store the client registration
|
|
102
|
+
registeredClients.set(clientId, {
|
|
103
|
+
client_id: clientId,
|
|
104
|
+
client_name: body.client_name || 'MCP Client',
|
|
105
|
+
redirect_uris: body.redirect_uris || [],
|
|
106
|
+
grant_types: body.grant_types || ['authorization_code', 'refresh_token'],
|
|
107
|
+
response_types: body.response_types || ['code'],
|
|
108
|
+
scope: body.scope,
|
|
109
|
+
token_endpoint_auth_method: 'none',
|
|
110
|
+
created_at: Date.now()
|
|
111
|
+
});
|
|
112
|
+
// Return the client registration response
|
|
113
|
+
res.status(201).json({
|
|
114
|
+
client_id: clientId,
|
|
115
|
+
client_name: body.client_name || 'MCP Client',
|
|
116
|
+
redirect_uris: body.redirect_uris || [],
|
|
117
|
+
grant_types: body.grant_types || ['authorization_code', 'refresh_token'],
|
|
118
|
+
response_types: body.response_types || ['code'],
|
|
119
|
+
scope: body.scope,
|
|
120
|
+
token_endpoint_auth_method: 'none'
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
// Authorization endpoint - redirects to Microsoft
|
|
124
|
+
app.get('/authorize', async (req, res) => {
|
|
125
|
+
const url = new URL(req.url, `${req.protocol}://${req.get('host')}`);
|
|
126
|
+
const tenantId = process.env.MS365_MCP_TENANT_ID || 'common';
|
|
127
|
+
const clientId = process.env.MS365_MCP_CLIENT_ID || '084a3e9f-a9f4-43f7-89f9-d229cf97853e';
|
|
128
|
+
const microsoftAuthUrl = new URL(`https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/authorize`);
|
|
129
|
+
// Only forward parameters that Microsoft OAuth 2.0 v2.0 supports
|
|
130
|
+
const allowedParams = [
|
|
131
|
+
'response_type', 'redirect_uri', 'scope', 'state', 'response_mode',
|
|
132
|
+
'code_challenge', 'code_challenge_method', 'prompt', 'login_hint', 'domain_hint'
|
|
133
|
+
];
|
|
134
|
+
allowedParams.forEach(param => {
|
|
135
|
+
const value = url.searchParams.get(param);
|
|
136
|
+
if (value) {
|
|
137
|
+
microsoftAuthUrl.searchParams.set(param, value);
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
// Use our Microsoft app's client_id
|
|
141
|
+
microsoftAuthUrl.searchParams.set('client_id', clientId);
|
|
142
|
+
// Ensure we have the minimal required scopes if none provided
|
|
143
|
+
if (!microsoftAuthUrl.searchParams.get('scope')) {
|
|
144
|
+
microsoftAuthUrl.searchParams.set('scope', 'User.Read Files.Read Mail.Read');
|
|
145
|
+
}
|
|
146
|
+
// Redirect to Microsoft's authorization page
|
|
147
|
+
res.redirect(microsoftAuthUrl.toString());
|
|
148
|
+
});
|
|
149
|
+
// Token exchange endpoint
|
|
150
|
+
app.post('/token', async (req, res) => {
|
|
151
|
+
try {
|
|
152
|
+
// Comprehensive debugging
|
|
153
|
+
logger.info('Token endpoint called', {
|
|
154
|
+
method: req.method,
|
|
155
|
+
url: req.url,
|
|
156
|
+
headers: req.headers,
|
|
157
|
+
bodyType: typeof req.body,
|
|
158
|
+
body: req.body,
|
|
159
|
+
rawBody: JSON.stringify(req.body),
|
|
160
|
+
contentType: req.get('Content-Type')
|
|
161
|
+
});
|
|
162
|
+
const body = req.body;
|
|
163
|
+
// Add debugging and validation
|
|
164
|
+
if (!body) {
|
|
165
|
+
logger.error('Token endpoint: Request body is undefined');
|
|
166
|
+
res.status(400).json({
|
|
167
|
+
error: 'invalid_request',
|
|
168
|
+
error_description: 'Request body is required'
|
|
169
|
+
});
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
if (!body.grant_type) {
|
|
173
|
+
logger.error('Token endpoint: grant_type is missing', { body });
|
|
174
|
+
res.status(400).json({
|
|
175
|
+
error: 'invalid_request',
|
|
176
|
+
error_description: 'grant_type parameter is required'
|
|
177
|
+
});
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
if (body.grant_type === 'authorization_code') {
|
|
181
|
+
const tenantId = process.env.MS365_MCP_TENANT_ID || 'common';
|
|
182
|
+
const clientId = process.env.MS365_MCP_CLIENT_ID || '084a3e9f-a9f4-43f7-89f9-d229cf97853e';
|
|
183
|
+
const clientSecret = process.env.MS365_MCP_CLIENT_SECRET;
|
|
184
|
+
if (!clientSecret) {
|
|
185
|
+
logger.error('Token endpoint: MS365_MCP_CLIENT_SECRET is not configured');
|
|
186
|
+
res.status(500).json({
|
|
187
|
+
error: 'server_error',
|
|
188
|
+
error_description: 'Server configuration error'
|
|
189
|
+
});
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
const result = await exchangeCodeForToken(body.code, body.redirect_uri, clientId, clientSecret, tenantId, body.code_verifier);
|
|
193
|
+
res.json(result);
|
|
194
|
+
}
|
|
195
|
+
else if (body.grant_type === 'refresh_token') {
|
|
196
|
+
const tenantId = process.env.MS365_MCP_TENANT_ID || 'common';
|
|
197
|
+
const clientId = process.env.MS365_MCP_CLIENT_ID || '084a3e9f-a9f4-43f7-89f9-d229cf97853e';
|
|
198
|
+
const clientSecret = process.env.MS365_MCP_CLIENT_SECRET;
|
|
199
|
+
if (!clientSecret) {
|
|
200
|
+
logger.error('Token endpoint: MS365_MCP_CLIENT_SECRET is not configured');
|
|
201
|
+
res.status(500).json({
|
|
202
|
+
error: 'server_error',
|
|
203
|
+
error_description: 'Server configuration error'
|
|
204
|
+
});
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
const result = await refreshAccessToken(body.refresh_token, clientId, clientSecret, tenantId);
|
|
208
|
+
res.json(result);
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
res.status(400).json({
|
|
212
|
+
error: 'unsupported_grant_type',
|
|
213
|
+
error_description: `Grant type '${body.grant_type}' is not supported`
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
catch (error) {
|
|
218
|
+
logger.error('Token endpoint error:', error);
|
|
219
|
+
res.status(500).json({
|
|
220
|
+
error: 'server_error',
|
|
221
|
+
error_description: 'Internal server error during token exchange'
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
});
|
|
43
225
|
app.use(mcpAuthRouter({
|
|
44
226
|
provider: oauthProvider,
|
|
45
227
|
issuerUrl: new URL(`http://localhost:${port}`),
|
|
46
228
|
}));
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
return requireBearerAuth({ provider: oauthProvider })(req, res, next);
|
|
50
|
-
}
|
|
51
|
-
next();
|
|
52
|
-
}, async (req, res) => {
|
|
229
|
+
// Microsoft Graph MCP endpoints with bearer token auth
|
|
230
|
+
app.post('/mcp', microsoftBearerTokenAuthMiddleware, async (req, res) => {
|
|
53
231
|
try {
|
|
232
|
+
// Set OAuth tokens in the GraphClient if available
|
|
233
|
+
if (req.microsoftAuth) {
|
|
234
|
+
this.graphClient.setOAuthTokens(req.microsoftAuth.accessToken, req.microsoftAuth.refreshToken);
|
|
235
|
+
}
|
|
54
236
|
const transport = new StreamableHTTPServerTransport({
|
|
55
237
|
sessionIdGenerator: undefined, // Stateless mode
|
|
56
238
|
});
|
|
@@ -74,10 +256,15 @@ class MicrosoftGraphServer {
|
|
|
74
256
|
}
|
|
75
257
|
}
|
|
76
258
|
});
|
|
259
|
+
// Health check endpoint
|
|
260
|
+
app.get('/', (req, res) => {
|
|
261
|
+
res.send('Microsoft 365 MCP Server is running');
|
|
262
|
+
});
|
|
77
263
|
app.listen(port, () => {
|
|
78
264
|
logger.info(`Server listening on HTTP port ${port}`);
|
|
79
265
|
logger.info(` - MCP endpoint: http://localhost:${port}/mcp`);
|
|
80
266
|
logger.info(` - OAuth endpoints: http://localhost:${port}/auth/*`);
|
|
267
|
+
logger.info(` - OAuth discovery: http://localhost:${port}/.well-known/oauth-authorization-server`);
|
|
81
268
|
});
|
|
82
269
|
}
|
|
83
270
|
else {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@softeria/ms-365-mcp-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.1",
|
|
4
4
|
"description": "Microsoft 365 MCP Server",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
"test": "vitest run",
|
|
13
13
|
"test:watch": "vitest",
|
|
14
14
|
"dev": "tsx src/index.ts",
|
|
15
|
+
"dev:http": "tsx --watch src/index.ts --http 3000 -v",
|
|
15
16
|
"format": "prettier --write \"**/*.{ts,mts,js,mjs,json,md}\"",
|
|
16
17
|
"release": "ts-node --esm bin/release.mts",
|
|
17
18
|
"inspect": "npx @modelcontextprotocol/inspector tsx src/index.ts",
|
|
@@ -32,6 +33,7 @@
|
|
|
32
33
|
"@azure/msal-node": "^2.1.0",
|
|
33
34
|
"@modelcontextprotocol/sdk": "^1.8.0",
|
|
34
35
|
"commander": "^11.1.0",
|
|
36
|
+
"dotenv": "^17.0.1",
|
|
35
37
|
"express": "^5.1.0",
|
|
36
38
|
"js-yaml": "^4.1.0",
|
|
37
39
|
"keytar": "^7.9.0",
|