@softeria/ms-365-mcp-server 0.9.13 → 0.10.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/.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:
@@ -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
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ import 'dotenv/config';
2
3
  import { parseArgs } from './cli.js';
3
4
  import logger from './logger.js';
4
5
  import AuthManager from './auth.js';
@@ -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
- app.post('/mcp', async (req, res, next) => {
48
- if (req.headers.authorization?.startsWith('Bearer ')) {
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.9.13",
3
+ "version": "0.10.0",
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",