@softeria/ms-365-mcp-server 0.6.2 → 0.8.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/README.md CHANGED
@@ -67,18 +67,54 @@ integration method.
67
67
 
68
68
  > ⚠️ You must authenticate before using tools.
69
69
 
70
- 1. **MCP client login**:
70
+ The server supports two authentication methods:
71
+
72
+ #### 1. Device Code Flow (Default)
73
+
74
+ For interactive authentication via device code:
75
+
76
+ - **MCP client login**:
71
77
  - Call the `login` tool (auto-checks existing token)
72
78
  - If needed, get URL+code, visit in browser
73
79
  - Use `verify-login` tool to confirm
74
- 2. **Optional CLI login**:
80
+ - **CLI login**:
75
81
  ```bash
76
82
  npx @softeria/ms-365-mcp-server --login
77
83
  ```
78
- Follow the URL and code prompt in the terminal.
84
+ Follow the URL and code prompt in the terminal.
79
85
 
80
86
  Tokens are cached securely in your OS credential store (fallback to file).
81
87
 
88
+ #### 2. OAuth Authorization Code Flow (HTTP mode only)
89
+
90
+ When running with `--http`, the server **requires** OAuth authentication:
91
+
92
+ ```bash
93
+ npx @softeria/ms-365-mcp-server --http 3000
94
+ ```
95
+
96
+ This mode:
97
+
98
+ - Advertises OAuth capabilities to MCP clients
99
+ - Provides OAuth endpoints at `/auth/*` (authorize, token, metadata)
100
+ - **Requires** `Authorization: Bearer <token>` for all MCP requests
101
+ - Validates tokens with Microsoft Graph API
102
+ - **Disables** login/logout tools by default (use `--enable-auth-tools` to enable them)
103
+
104
+ MCP clients will automatically handle the OAuth flow when they see the advertised capabilities. For manual testing:
105
+
106
+ ```bash
107
+ curl -X POST http://localhost:3000/mcp \
108
+ -H "Authorization: Bearer YOUR_MICROSOFT_ACCESS_TOKEN" \
109
+ -H "Content-Type: application/json" \
110
+ -d '{"jsonrpc": "2.0", "method": "initialize", "params": {"protocolVersion": "1.0.0", "capabilities": {}}, "id": 1}'
111
+ ```
112
+
113
+ > **Note**: HTTP mode requires authentication. For unauthenticated testing, use stdio mode with device code flow.
114
+ >
115
+ > **Authentication Tools**: In HTTP mode, login/logout tools are disabled by default since OAuth handles authentication.
116
+ > Use `--enable-auth-tools` if you need them available.
117
+
82
118
  ## CLI Options
83
119
 
84
120
  The following options can be used when running ms-365-mcp-server directly from the command line:
@@ -98,6 +134,7 @@ When running as an MCP server, the following options can be used:
98
134
  --read-only Start server in read-only mode, disabling write operations
99
135
  --http [port] Use Streamable HTTP transport instead of stdio (optionally specify port, default: 3000)
100
136
  Starts Express.js server with MCP endpoint at /mcp
137
+ --enable-auth-tools Enable login/logout tools when using HTTP mode (disabled by default in HTTP mode)
101
138
  ```
102
139
 
103
140
  Environment variables:
@@ -31,6 +31,7 @@ export function createAndSaveSimplifiedOpenAPI(endpointsFile, openapiFile, opena
31
31
 
32
32
  if (openApiSpec.components && openApiSpec.components.schemas) {
33
33
  removeODataTypeRecursively(openApiSpec.components.schemas);
34
+ flattenComplexSchemasRecursively(openApiSpec.components.schemas);
34
35
  }
35
36
 
36
37
  fs.writeFileSync(openapiTrimmedFile, yaml.dump(openApiSpec));
@@ -83,3 +84,156 @@ function removeODataTypeRecursively(obj) {
83
84
  }
84
85
  });
85
86
  }
87
+
88
+ function flattenComplexSchemasRecursively(schemas) {
89
+ console.log('Flattening complex schemas for better client compatibility...');
90
+
91
+ let flattenedCount = 0;
92
+
93
+ Object.keys(schemas).forEach((schemaName) => {
94
+ const schema = schemas[schemaName];
95
+
96
+ if (schema.allOf && Array.isArray(schema.allOf) && schema.allOf.length <= 5) {
97
+ try {
98
+ const flattened = { type: 'object', properties: {} };
99
+ const required = new Set();
100
+
101
+ for (const subSchema of schema.allOf) {
102
+ if (subSchema.$ref && subSchema.$ref.startsWith('#/components/schemas/')) {
103
+ const refName = subSchema.$ref.replace('#/components/schemas/', '');
104
+ if (schemas[refName] && schemas[refName].properties) {
105
+ Object.assign(flattened.properties, schemas[refName].properties);
106
+ if (schemas[refName].required) {
107
+ schemas[refName].required.forEach((req) => required.add(req));
108
+ }
109
+ }
110
+ } else if (subSchema.properties) {
111
+ Object.assign(flattened.properties, subSchema.properties);
112
+ if (subSchema.required) {
113
+ subSchema.required.forEach((req) => required.add(req));
114
+ }
115
+ }
116
+
117
+ Object.keys(subSchema).forEach((key) => {
118
+ if (!['allOf', 'properties', 'required', '$ref'].includes(key) && !flattened[key]) {
119
+ flattened[key] = subSchema[key];
120
+ }
121
+ });
122
+ }
123
+
124
+ if (schema.properties) {
125
+ Object.assign(flattened.properties, schema.properties);
126
+ }
127
+
128
+ if (schema.required) {
129
+ schema.required.forEach((req) => required.add(req));
130
+ }
131
+
132
+ Object.keys(schema).forEach((key) => {
133
+ if (!['allOf', 'properties', 'required'].includes(key) && !flattened[key]) {
134
+ flattened[key] = schema[key];
135
+ }
136
+ });
137
+
138
+ if (required.size > 0) {
139
+ flattened.required = Array.from(required);
140
+ }
141
+
142
+ schemas[schemaName] = flattened;
143
+ flattenedCount++;
144
+ } catch (error) {
145
+ console.warn(`Warning: Could not flatten schema ${schemaName}:`, error.message);
146
+ }
147
+ }
148
+
149
+ if (schema.anyOf && Array.isArray(schema.anyOf) && schema.anyOf.length > 2) {
150
+ console.log(`Simplifying anyOf in ${schemaName} (${schema.anyOf.length} -> 1 option)`);
151
+ const simplified = { ...schema.anyOf[0] };
152
+ simplified.nullable = true;
153
+ simplified.description = `Simplified from ${schema.anyOf.length} anyOf options`;
154
+ schemas[schemaName] = simplified;
155
+ flattenedCount++;
156
+ }
157
+
158
+ if (schema.oneOf && Array.isArray(schema.oneOf) && schema.oneOf.length > 2) {
159
+ console.log(`Simplifying oneOf in ${schemaName} (${schema.oneOf.length} -> 1 option)`);
160
+ const simplified = { ...schema.oneOf[0] };
161
+ simplified.nullable = true;
162
+ simplified.description = `Simplified from ${schema.oneOf.length} oneOf options`;
163
+ schemas[schemaName] = simplified;
164
+ flattenedCount++;
165
+ }
166
+
167
+ if (schema.properties && Object.keys(schema.properties).length > 25) {
168
+ console.log(
169
+ `Reducing properties in ${schemaName} (${Object.keys(schema.properties).length} -> 25)`
170
+ );
171
+ const priorityProperties = {};
172
+ const allKeys = Object.keys(schema.properties);
173
+
174
+ if (schema.required) {
175
+ schema.required.forEach((key) => {
176
+ if (schema.properties[key]) {
177
+ priorityProperties[key] = schema.properties[key];
178
+ }
179
+ });
180
+ }
181
+
182
+ const remainingSlots = 25 - Object.keys(priorityProperties).length;
183
+ allKeys.slice(0, remainingSlots).forEach((key) => {
184
+ if (!priorityProperties[key]) {
185
+ priorityProperties[key] = schema.properties[key];
186
+ }
187
+ });
188
+
189
+ schema.properties = priorityProperties;
190
+ schema.description =
191
+ `${schema.description || ''} [Simplified: showing ${Object.keys(priorityProperties).length} of ${allKeys.length} properties]`.trim();
192
+ flattenedCount++;
193
+ }
194
+
195
+ if (schema.properties) {
196
+ simplifyNestedPropertiesRecursively(schema.properties, 0, 4);
197
+ }
198
+ });
199
+
200
+ console.log(`Flattened ${flattenedCount} complex schemas`);
201
+ }
202
+
203
+ function simplifyNestedPropertiesRecursively(properties, currentDepth, maxDepth) {
204
+ if (currentDepth >= maxDepth) {
205
+ return;
206
+ }
207
+
208
+ Object.keys(properties).forEach((key) => {
209
+ const prop = properties[key];
210
+
211
+ if (prop && typeof prop === 'object') {
212
+ if (currentDepth === maxDepth - 1 && prop.properties) {
213
+ console.log(`Flattening nested property at depth ${currentDepth}: ${key}`);
214
+ prop.type = 'object';
215
+ prop.description = `${prop.description || ''} [Simplified: nested object]`.trim();
216
+ delete prop.properties;
217
+ delete prop.additionalProperties;
218
+ } else if (prop.properties) {
219
+ simplifyNestedPropertiesRecursively(prop.properties, currentDepth + 1, maxDepth);
220
+ }
221
+
222
+ if (prop.anyOf && Array.isArray(prop.anyOf) && prop.anyOf.length > 2) {
223
+ prop.type = prop.anyOf[0].type || 'object';
224
+ prop.nullable = true;
225
+ prop.description =
226
+ `${prop.description || ''} [Simplified from ${prop.anyOf.length} options]`.trim();
227
+ delete prop.anyOf;
228
+ }
229
+
230
+ if (prop.oneOf && Array.isArray(prop.oneOf) && prop.oneOf.length > 2) {
231
+ prop.type = prop.oneOf[0].type || 'object';
232
+ prop.nullable = true;
233
+ prop.description =
234
+ `${prop.description || ''} [Simplified from ${prop.oneOf.length} options]`.trim();
235
+ delete prop.oneOf;
236
+ }
237
+ }
238
+ });
239
+ }
package/dist/auth.js CHANGED
@@ -47,6 +47,8 @@ class AuthManager {
47
47
  this.msalApp = new PublicClientApplication(this.config);
48
48
  this.accessToken = null;
49
49
  this.tokenExpiry = null;
50
+ this.oauthToken = null;
51
+ this.isOAuthMode = false;
50
52
  }
51
53
  async loadTokenCache() {
52
54
  try {
@@ -86,7 +88,14 @@ class AuthManager {
86
88
  logger.error(`Error saving token cache: ${error.message}`);
87
89
  }
88
90
  }
91
+ async setOAuthToken(token) {
92
+ this.oauthToken = token;
93
+ this.isOAuthMode = true;
94
+ }
89
95
  async getToken(forceRefresh = false) {
96
+ if (this.isOAuthMode && this.oauthToken) {
97
+ return this.oauthToken;
98
+ }
90
99
  if (this.accessToken && this.tokenExpiry && this.tokenExpiry > Date.now() && !forceRefresh) {
91
100
  return this.accessToken;
92
101
  }
@@ -103,7 +112,8 @@ class AuthManager {
103
112
  return this.accessToken;
104
113
  }
105
114
  catch (error) {
106
- logger.info('Silent token acquisition failed, using device code flow');
115
+ logger.error('Silent token acquisition failed');
116
+ throw new Error('Silent token acquisition failed');
107
117
  }
108
118
  }
109
119
  throw new Error('No valid token found');
package/dist/cli.js CHANGED
@@ -16,7 +16,8 @@ program
16
16
  .option('--logout', 'Log out and clear saved credentials')
17
17
  .option('--verify-login', 'Verify login without starting the server')
18
18
  .option('--read-only', 'Start server in read-only mode, disabling write operations')
19
- .option('--http [port]', 'Use Streamable HTTP transport instead of stdio (optionally specify port, default: 3000)');
19
+ .option('--http [port]', 'Use Streamable HTTP transport instead of stdio (optionally specify port, default: 3000)')
20
+ .option('--enable-auth-tools', 'Enable login/logout tools when using HTTP mode (disabled by default in HTTP mode)');
20
21
  export function parseArgs() {
21
22
  program.parse();
22
23
  const options = program.opts();