@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 +40 -3
- package/bin/modules/simplified-openapi.mjs +154 -0
- package/dist/auth.js +11 -1
- package/dist/cli.js +2 -1
- package/dist/generated/client.js +1704 -1629
- package/dist/oauth-provider.js +48 -0
- package/dist/server.js +21 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -67,18 +67,54 @@ integration method.
|
|
|
67
67
|
|
|
68
68
|
> ⚠️ You must authenticate before using tools.
|
|
69
69
|
|
|
70
|
-
|
|
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
|
-
|
|
80
|
+
- **CLI login**:
|
|
75
81
|
```bash
|
|
76
82
|
npx @softeria/ms-365-mcp-server --login
|
|
77
83
|
```
|
|
78
|
-
|
|
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.
|
|
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();
|