@softeria/ms-365-mcp-server 0.3.4 → 0.4.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/.github/workflows/build.yml +3 -0
- package/.github/workflows/npm-publish.yml +2 -0
- package/README.md +4 -15
- package/bin/generate-graph-client.mjs +59 -0
- package/bin/{download-openapi.mjs → modules/download-openapi.mjs} +10 -20
- package/bin/modules/extract-descriptions.mjs +48 -0
- package/bin/modules/generate-mcp-tools.mjs +36 -0
- package/bin/modules/simplified-openapi.mjs +78 -0
- package/dist/auth-tools.js +80 -0
- package/dist/auth.js +219 -0
- package/dist/cli.js +21 -0
- package/dist/endpoints.json +375 -0
- package/dist/generated/client.js +14683 -0
- package/dist/generated/endpoint-types.js +1 -0
- package/dist/generated/hack.js +37 -0
- package/dist/graph-client.js +254 -0
- package/dist/graph-tools.js +98 -0
- package/dist/index.js +39 -0
- package/dist/logger.js +33 -0
- package/dist/server.js +32 -0
- package/{src/version.mjs → dist/version.js} +0 -2
- package/package.json +12 -9
- package/src/{auth-tools.mjs → auth-tools.ts} +7 -5
- package/src/{auth.mjs → auth.ts} +60 -30
- package/src/{cli.mjs → cli.ts} +9 -1
- package/src/endpoints.json +375 -0
- package/src/generated/README.md +51 -0
- package/src/generated/client.ts +24916 -0
- package/src/generated/endpoint-types.ts +27 -0
- package/src/generated/hack.ts +50 -0
- package/src/{graph-client.mjs → graph-client.ts} +53 -18
- package/src/graph-tools.ts +174 -0
- package/{index.mjs → src/index.ts} +6 -6
- package/src/{logger.mjs → logger.ts} +1 -1
- package/src/{server.mjs → server.ts} +16 -9
- package/src/version.ts +9 -0
- package/test/{auth-tools.test.js → auth-tools.test.ts} +41 -38
- package/test/{cli.test.js → cli.test.ts} +3 -3
- package/test/{graph-api.test.js → graph-api.test.ts} +5 -5
- package/test/test-hack.ts +17 -0
- package/tsconfig.json +16 -0
- package/src/dynamic-tools.mjs +0 -442
- package/src/openapi-helpers.mjs +0 -187
- package/src/param-mapper.mjs +0 -30
- package/test/dynamic-tools.test.js +0 -852
- package/test/mappings.test.js +0 -29
- package/test/mcp-server.test.js +0 -36
- package/test/openapi-helpers.test.js +0 -210
- package/test/param-mapper.test.js +0 -56
|
@@ -13,6 +13,7 @@ jobs:
|
|
|
13
13
|
with:
|
|
14
14
|
node-version: 20
|
|
15
15
|
- run: npm ci
|
|
16
|
+
- run: npm run build
|
|
16
17
|
- run: npm test
|
|
17
18
|
|
|
18
19
|
publish-npm:
|
|
@@ -25,6 +26,7 @@ jobs:
|
|
|
25
26
|
node-version: 20
|
|
26
27
|
registry-url: https://registry.npmjs.org/
|
|
27
28
|
- run: npm ci
|
|
29
|
+
- run: npm run build
|
|
28
30
|
- run: npm publish
|
|
29
31
|
env:
|
|
30
32
|
NODE_AUTH_TOKEN: ${{secrets.npm_token}}
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# ms-365-mcp-server
|
|
2
2
|
|
|
3
|
-
  
|
|
3
|
+
[](https://www.npmjs.com/package/@softeria/ms-365-mcp-server) [](https://github.com/softeria/ms-365-mcp-server/actions/workflows/build.yml) [](https://github.com/softeria/ms-365-mcp-server/blob/main/LICENSE)
|
|
4
4
|
|
|
5
5
|
Microsoft 365 MCP Server
|
|
6
6
|
|
|
@@ -17,13 +17,11 @@ A Model Context Protocol (MCP) server for interacting with Microsoft 365 service
|
|
|
17
17
|
- Calendar event management
|
|
18
18
|
- Mail operations
|
|
19
19
|
- OneDrive file management
|
|
20
|
-
- Microsoft Teams integration
|
|
21
20
|
- OneNote notebooks and pages
|
|
22
21
|
- To Do tasks and task lists
|
|
23
22
|
- Planner plans and tasks
|
|
24
|
-
- SharePoint sites and lists
|
|
25
23
|
- Outlook contacts
|
|
26
|
-
- User
|
|
24
|
+
- User management
|
|
27
25
|
- Dynamic tools powered by Microsoft Graph OpenAPI spec
|
|
28
26
|
- Built on the Model Context Protocol
|
|
29
27
|
|
|
@@ -31,7 +29,7 @@ A Model Context Protocol (MCP) server for interacting with Microsoft 365 service
|
|
|
31
29
|
|
|
32
30
|
Test login in Claude Desktop:
|
|
33
31
|
|
|
34
|
-

|
|
35
33
|
|
|
36
34
|
## Examples
|
|
37
35
|
|
|
@@ -76,23 +74,14 @@ integration method.
|
|
|
76
74
|
- Call the `login` tool (auto-checks existing token)
|
|
77
75
|
- If needed, get URL+code, visit in browser
|
|
78
76
|
- Use `verify-login` tool to confirm
|
|
79
|
-
-
|
|
80
77
|
2. **Optional CLI login**:
|
|
81
78
|
```bash
|
|
82
79
|
npx @softeria/ms-365-mcp-server --login
|
|
83
80
|
```
|
|
84
|
-
Follow the URL and code prompt in terminal.
|
|
81
|
+
Follow the URL and code prompt in the terminal.
|
|
85
82
|
|
|
86
83
|
Tokens are cached securely in your OS credential store (fallback to file).
|
|
87
84
|
|
|
88
|
-
## Tools
|
|
89
|
-
|
|
90
|
-
- **Authentication:** `login`, `logout`, `verify-login`
|
|
91
|
-
- **Excel:** list worksheets, get/set ranges, format, sort, chart
|
|
92
|
-
- **Calendar:** list/create/update/delete events
|
|
93
|
-
- **Mail:** send, read, delete messages
|
|
94
|
-
- **OneDrive:** upload, download, list files
|
|
95
|
-
|
|
96
85
|
## License
|
|
97
86
|
|
|
98
87
|
MIT © 2025 Softeria
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
import { downloadGraphOpenAPI } from './modules/download-openapi.mjs';
|
|
6
|
+
import { generateMcpTools } from './modules/generate-mcp-tools.mjs';
|
|
7
|
+
import { createAndSaveSimplifiedOpenAPI } from './modules/simplified-openapi.mjs';
|
|
8
|
+
|
|
9
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
10
|
+
const __dirname = path.dirname(__filename);
|
|
11
|
+
const rootDir = path.resolve(__dirname, '..');
|
|
12
|
+
const openapiDir = path.join(rootDir, 'openapi');
|
|
13
|
+
const srcDir = path.join(rootDir, 'src');
|
|
14
|
+
|
|
15
|
+
const openapiFile = path.join(openapiDir, 'openapi.yaml');
|
|
16
|
+
const openapiTrimmedFile = path.join(openapiDir, 'openapi-trimmed.yaml');
|
|
17
|
+
const endpointsFile = path.join(srcDir, 'endpoints.json');
|
|
18
|
+
|
|
19
|
+
const generatedDir = path.join(srcDir, 'generated');
|
|
20
|
+
|
|
21
|
+
const args = process.argv.slice(2);
|
|
22
|
+
const forceDownload = args.includes('--force');
|
|
23
|
+
|
|
24
|
+
async function main() {
|
|
25
|
+
console.log('Microsoft Graph API OpenAPI Processor');
|
|
26
|
+
console.log('------------------------------------');
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
console.log('\n📥 Step 1: Downloading OpenAPI specification');
|
|
30
|
+
const downloaded = await downloadGraphOpenAPI(
|
|
31
|
+
openapiDir,
|
|
32
|
+
openapiFile,
|
|
33
|
+
undefined,
|
|
34
|
+
forceDownload
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
if (downloaded) {
|
|
38
|
+
console.log('\n✅ OpenAPI specification successfully downloaded');
|
|
39
|
+
} else {
|
|
40
|
+
console.log('\n⏭️ Download skipped (file exists)');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
console.log('\n🔧 Step 2: Creating simplified OpenAPI specification');
|
|
44
|
+
createAndSaveSimplifiedOpenAPI(endpointsFile, openapiFile, openapiTrimmedFile);
|
|
45
|
+
console.log('✅ Successfully created simplified OpenAPI specification');
|
|
46
|
+
|
|
47
|
+
console.log('\n🚀 Step 3: Generating client code using openapi-zod-client');
|
|
48
|
+
generateMcpTools(null, generatedDir);
|
|
49
|
+
console.log('✅ Successfully generated client code');
|
|
50
|
+
} catch (error) {
|
|
51
|
+
console.error('\n❌ Error processing OpenAPI specification:', error.message);
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
main().catch((error) => {
|
|
57
|
+
console.error('Fatal error:', error);
|
|
58
|
+
process.exit(1);
|
|
59
|
+
});
|
|
@@ -1,22 +1,13 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
1
|
import fs from 'fs';
|
|
4
|
-
import path from 'path';
|
|
5
|
-
import { fileURLToPath } from 'url';
|
|
6
|
-
|
|
7
|
-
const args = process.argv.slice(2);
|
|
8
|
-
const forceDownload = args.includes('--force');
|
|
9
|
-
|
|
10
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
11
|
-
const __dirname = path.dirname(__filename);
|
|
12
2
|
|
|
13
|
-
const
|
|
14
|
-
const targetFile = path.join(targetDir, 'openapi.yaml');
|
|
3
|
+
const DEFAULT_OPENAPI_URL = 'https://raw.githubusercontent.com/microsoftgraph/msgraph-metadata/refs/heads/master/openapi/v1.0/openapi.yaml';
|
|
15
4
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
5
|
+
export async function downloadGraphOpenAPI(
|
|
6
|
+
targetDir,
|
|
7
|
+
targetFile,
|
|
8
|
+
openapiUrl = DEFAULT_OPENAPI_URL,
|
|
9
|
+
forceDownload = false
|
|
10
|
+
) {
|
|
20
11
|
if (!fs.existsSync(targetDir)) {
|
|
21
12
|
console.log(`Creating directory: ${targetDir}`);
|
|
22
13
|
fs.mkdirSync(targetDir, { recursive: true });
|
|
@@ -25,7 +16,7 @@ async function downloadOpenApi() {
|
|
|
25
16
|
if (fs.existsSync(targetFile) && !forceDownload) {
|
|
26
17
|
console.log(`OpenAPI specification already exists at ${targetFile}`);
|
|
27
18
|
console.log('Use --force to download again');
|
|
28
|
-
return;
|
|
19
|
+
return false;
|
|
29
20
|
}
|
|
30
21
|
|
|
31
22
|
console.log(`Downloading OpenAPI specification from ${openapiUrl}`);
|
|
@@ -40,10 +31,9 @@ async function downloadOpenApi() {
|
|
|
40
31
|
const content = await response.text();
|
|
41
32
|
fs.writeFileSync(targetFile, content);
|
|
42
33
|
console.log(`OpenAPI specification downloaded to ${targetFile}`);
|
|
34
|
+
return true;
|
|
43
35
|
} catch (error) {
|
|
44
36
|
console.error('Error downloading OpenAPI specification:', error.message);
|
|
45
|
-
|
|
37
|
+
throw error;
|
|
46
38
|
}
|
|
47
39
|
}
|
|
48
|
-
|
|
49
|
-
downloadOpenApi();
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import yaml from 'js-yaml';
|
|
3
|
+
|
|
4
|
+
export function convertPathToOpenApiFormat(pathPattern) {
|
|
5
|
+
let path = pathPattern.replace(/\{([^}]+)\}/g, (match, param) => {
|
|
6
|
+
const normalizedParam = param.replace(/-/g, '_');
|
|
7
|
+
return `{${normalizedParam}}`;
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
path = path.replace(/\{([^}]+)_id(\d+)\}/g, (match, param, num) => {
|
|
11
|
+
return `{${param}_id_${num}}`;
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
if (!path.startsWith('/')) {
|
|
15
|
+
path = '/' + path;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return path;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function extractDescriptions(openapiFile, endpoints) {
|
|
22
|
+
console.log('Extracting descriptions from OpenAPI spec...');
|
|
23
|
+
|
|
24
|
+
const openApiSpec = yaml.load(fs.readFileSync(openapiFile, 'utf8'));
|
|
25
|
+
const descriptions = {};
|
|
26
|
+
|
|
27
|
+
endpoints.forEach((endpoint) => {
|
|
28
|
+
const path = convertPathToOpenApiFormat(endpoint.pathPattern);
|
|
29
|
+
const method = endpoint.method.toLowerCase();
|
|
30
|
+
|
|
31
|
+
if (openApiSpec.paths && openApiSpec.paths[path] && openApiSpec.paths[path][method]) {
|
|
32
|
+
const operation = openApiSpec.paths[path][method];
|
|
33
|
+
|
|
34
|
+
const description =
|
|
35
|
+
operation.description || operation.summary || `Operation for ${endpoint.toolName}`;
|
|
36
|
+
|
|
37
|
+
descriptions[endpoint.toolName] = description;
|
|
38
|
+
console.log(
|
|
39
|
+
`Found description for ${endpoint.toolName}: ${description.substring(0, 50)}${description.length > 50 ? '...' : ''}`
|
|
40
|
+
);
|
|
41
|
+
} else {
|
|
42
|
+
console.warn(`Path ${path} ${method} not found in OpenAPI spec`);
|
|
43
|
+
descriptions[endpoint.toolName] = `Operation for ${endpoint.toolName}`;
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
return descriptions;
|
|
48
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { execSync } from 'child_process';
|
|
4
|
+
|
|
5
|
+
export function generateMcpTools(openApiSpec, outputDir) {
|
|
6
|
+
try {
|
|
7
|
+
console.log('Generating client code from OpenAPI spec using openapi-zod-client...');
|
|
8
|
+
|
|
9
|
+
if (!fs.existsSync(outputDir)) {
|
|
10
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
11
|
+
console.log(`Created directory: ${outputDir}`);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const rootDir = path.resolve(outputDir, '../..');
|
|
15
|
+
const openapiDir = path.join(rootDir, 'openapi');
|
|
16
|
+
const openapiTrimmedFile = path.join(openapiDir, 'openapi-trimmed.yaml');
|
|
17
|
+
|
|
18
|
+
const clientFilePath = path.join(outputDir, 'client.ts');
|
|
19
|
+
execSync(
|
|
20
|
+
`npx -y openapi-zod-client ${openapiTrimmedFile} -o ${clientFilePath} --with-description`,
|
|
21
|
+
{
|
|
22
|
+
stdio: 'inherit',
|
|
23
|
+
}
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
console.log(`Generated client code at: ${clientFilePath}`);
|
|
27
|
+
|
|
28
|
+
let clientCode = fs.readFileSync(clientFilePath, 'utf-8');
|
|
29
|
+
clientCode = clientCode.replace(/'@zodios\/core';/, "'./hack.js';");
|
|
30
|
+
fs.writeFileSync(clientFilePath, clientCode);
|
|
31
|
+
|
|
32
|
+
return true;
|
|
33
|
+
} catch (error) {
|
|
34
|
+
throw new Error(`Error generating client code: ${error.message}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import yaml from 'js-yaml';
|
|
3
|
+
|
|
4
|
+
export function createAndSaveSimplifiedOpenAPI(endpointsFile, openapiFile, openapiTrimmedFile) {
|
|
5
|
+
const endpoints = JSON.parse(fs.readFileSync(endpointsFile, 'utf8'));
|
|
6
|
+
|
|
7
|
+
const spec = fs.readFileSync(openapiFile, 'utf8');
|
|
8
|
+
const openApiSpec = yaml.load(spec);
|
|
9
|
+
for (const [key, value] of Object.entries(openApiSpec.paths)) {
|
|
10
|
+
const e = endpoints.filter((ep) => ep.pathPattern === key);
|
|
11
|
+
if (e.length === 0) {
|
|
12
|
+
delete openApiSpec.paths[key];
|
|
13
|
+
} else {
|
|
14
|
+
for (const [method, operation] of Object.entries(value)) {
|
|
15
|
+
const eo = e.find((ep) => ep.method.toLowerCase() === method);
|
|
16
|
+
if (eo) {
|
|
17
|
+
operation.operationId = eo.toolName;
|
|
18
|
+
} else {
|
|
19
|
+
delete value[method];
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (openApiSpec.components && openApiSpec.components.schemas) {
|
|
26
|
+
removeODataTypeRecursively(openApiSpec.components.schemas);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
fs.writeFileSync(openapiTrimmedFile, yaml.dump(openApiSpec));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function removeODataTypeRecursively(obj) {
|
|
33
|
+
if (!obj || typeof obj !== 'object') return;
|
|
34
|
+
|
|
35
|
+
if (Array.isArray(obj)) {
|
|
36
|
+
obj.forEach((item) => removeODataTypeRecursively(item));
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (obj.properties && obj.properties['@odata.type']) {
|
|
41
|
+
delete obj.properties['@odata.type'];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (obj.required && Array.isArray(obj.required)) {
|
|
45
|
+
const typeIndex = obj.required.indexOf('@odata.type');
|
|
46
|
+
if (typeIndex !== -1) {
|
|
47
|
+
obj.required.splice(typeIndex, 1);
|
|
48
|
+
if (obj.required.length === 0) {
|
|
49
|
+
delete obj.required;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (obj.properties) {
|
|
55
|
+
removeODataTypeRecursively(obj.properties);
|
|
56
|
+
Object.values(obj.properties).forEach((prop) => removeODataTypeRecursively(prop));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (obj.additionalProperties && typeof obj.additionalProperties === 'object') {
|
|
60
|
+
removeODataTypeRecursively(obj.additionalProperties);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (obj.items) {
|
|
64
|
+
removeODataTypeRecursively(obj.items);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
['allOf', 'anyOf', 'oneOf'].forEach((key) => {
|
|
68
|
+
if (obj[key] && Array.isArray(obj[key])) {
|
|
69
|
+
obj[key].forEach((item) => removeODataTypeRecursively(item));
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
Object.keys(obj).forEach((key) => {
|
|
74
|
+
if (typeof obj[key] === 'object' && obj[key] !== null) {
|
|
75
|
+
removeODataTypeRecursively(obj[key]);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export function registerAuthTools(server, authManager) {
|
|
3
|
+
server.tool('login', {
|
|
4
|
+
force: z.boolean().default(false).describe('Force a new login even if already logged in'),
|
|
5
|
+
}, async ({ force }) => {
|
|
6
|
+
try {
|
|
7
|
+
if (!force) {
|
|
8
|
+
const loginStatus = await authManager.testLogin();
|
|
9
|
+
if (loginStatus.success) {
|
|
10
|
+
return {
|
|
11
|
+
content: [
|
|
12
|
+
{
|
|
13
|
+
type: 'text',
|
|
14
|
+
text: JSON.stringify({
|
|
15
|
+
status: 'Already logged in',
|
|
16
|
+
...loginStatus,
|
|
17
|
+
}),
|
|
18
|
+
},
|
|
19
|
+
],
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
const text = await new Promise((r) => {
|
|
24
|
+
authManager.acquireTokenByDeviceCode(r);
|
|
25
|
+
});
|
|
26
|
+
return {
|
|
27
|
+
content: [
|
|
28
|
+
{
|
|
29
|
+
type: 'text',
|
|
30
|
+
text,
|
|
31
|
+
},
|
|
32
|
+
],
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
return {
|
|
37
|
+
content: [
|
|
38
|
+
{
|
|
39
|
+
type: 'text',
|
|
40
|
+
text: JSON.stringify({ error: `Authentication failed: ${error.message}` }),
|
|
41
|
+
},
|
|
42
|
+
],
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
server.tool('logout', {}, async () => {
|
|
47
|
+
try {
|
|
48
|
+
await authManager.logout();
|
|
49
|
+
return {
|
|
50
|
+
content: [
|
|
51
|
+
{
|
|
52
|
+
type: 'text',
|
|
53
|
+
text: JSON.stringify({ message: 'Logged out successfully' }),
|
|
54
|
+
},
|
|
55
|
+
],
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
return {
|
|
60
|
+
content: [
|
|
61
|
+
{
|
|
62
|
+
type: 'text',
|
|
63
|
+
text: JSON.stringify({ error: 'Logout failed' }),
|
|
64
|
+
},
|
|
65
|
+
],
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
server.tool('verify-login', async () => {
|
|
70
|
+
const testResult = await authManager.testLogin();
|
|
71
|
+
return {
|
|
72
|
+
content: [
|
|
73
|
+
{
|
|
74
|
+
type: 'text',
|
|
75
|
+
text: JSON.stringify(testResult),
|
|
76
|
+
},
|
|
77
|
+
],
|
|
78
|
+
};
|
|
79
|
+
});
|
|
80
|
+
}
|
package/dist/auth.js
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { PublicClientApplication } from '@azure/msal-node';
|
|
2
|
+
import keytar from 'keytar';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import fs from 'fs';
|
|
6
|
+
import logger from './logger.js';
|
|
7
|
+
const endpoints = await import('./endpoints.json', {
|
|
8
|
+
assert: { type: 'json' },
|
|
9
|
+
});
|
|
10
|
+
const SERVICE_NAME = 'ms-365-mcp-server';
|
|
11
|
+
const TOKEN_CACHE_ACCOUNT = 'msal-token-cache';
|
|
12
|
+
const FALLBACK_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
const FALLBACK_PATH = path.join(FALLBACK_DIR, '..', '.token-cache.json');
|
|
14
|
+
const DEFAULT_CONFIG = {
|
|
15
|
+
auth: {
|
|
16
|
+
clientId: '084a3e9f-a9f4-43f7-89f9-d229cf97853e',
|
|
17
|
+
authority: 'https://login.microsoftonline.com/common',
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
const SCOPE_HIERARCHY = {
|
|
21
|
+
'Mail.ReadWrite': ['Mail.Read', 'Mail.Send'],
|
|
22
|
+
'Calendars.ReadWrite': ['Calendars.Read'],
|
|
23
|
+
'Files.ReadWrite': ['Files.Read'],
|
|
24
|
+
'Tasks.ReadWrite': ['Tasks.Read'],
|
|
25
|
+
'Contacts.ReadWrite': ['Contacts.Read'],
|
|
26
|
+
};
|
|
27
|
+
function buildScopesFromEndpoints() {
|
|
28
|
+
const scopesSet = new Set();
|
|
29
|
+
endpoints.default.forEach((endpoint) => {
|
|
30
|
+
if (endpoint.scopes && Array.isArray(endpoint.scopes)) {
|
|
31
|
+
endpoint.scopes.forEach((scope) => scopesSet.add(scope));
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
Object.entries(SCOPE_HIERARCHY).forEach(([higherScope, lowerScopes]) => {
|
|
35
|
+
if (lowerScopes.every((scope) => scopesSet.has(scope))) {
|
|
36
|
+
lowerScopes.forEach((scope) => scopesSet.delete(scope));
|
|
37
|
+
scopesSet.add(higherScope);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
return Array.from(scopesSet);
|
|
41
|
+
}
|
|
42
|
+
class AuthManager {
|
|
43
|
+
constructor(config = DEFAULT_CONFIG, scopes = buildScopesFromEndpoints()) {
|
|
44
|
+
logger.info(`And scopes are ${scopes.join(', ')}`, scopes);
|
|
45
|
+
this.config = config;
|
|
46
|
+
this.scopes = scopes;
|
|
47
|
+
this.msalApp = new PublicClientApplication(this.config);
|
|
48
|
+
this.accessToken = null;
|
|
49
|
+
this.tokenExpiry = null;
|
|
50
|
+
}
|
|
51
|
+
async loadTokenCache() {
|
|
52
|
+
try {
|
|
53
|
+
let cacheData;
|
|
54
|
+
try {
|
|
55
|
+
const cachedData = await keytar.getPassword(SERVICE_NAME, TOKEN_CACHE_ACCOUNT);
|
|
56
|
+
if (cachedData) {
|
|
57
|
+
cacheData = cachedData;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
catch (keytarError) {
|
|
61
|
+
logger.warn(`Keychain access failed, falling back to file storage: ${keytarError.message}`);
|
|
62
|
+
}
|
|
63
|
+
if (!cacheData && fs.existsSync(FALLBACK_PATH)) {
|
|
64
|
+
cacheData = fs.readFileSync(FALLBACK_PATH, 'utf8');
|
|
65
|
+
}
|
|
66
|
+
if (cacheData) {
|
|
67
|
+
this.msalApp.getTokenCache().deserialize(cacheData);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
logger.error(`Error loading token cache: ${error.message}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
async saveTokenCache() {
|
|
75
|
+
try {
|
|
76
|
+
const cacheData = this.msalApp.getTokenCache().serialize();
|
|
77
|
+
try {
|
|
78
|
+
await keytar.setPassword(SERVICE_NAME, TOKEN_CACHE_ACCOUNT, cacheData);
|
|
79
|
+
}
|
|
80
|
+
catch (keytarError) {
|
|
81
|
+
logger.warn(`Keychain save failed, falling back to file storage: ${keytarError.message}`);
|
|
82
|
+
fs.writeFileSync(FALLBACK_PATH, cacheData);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
logger.error(`Error saving token cache: ${error.message}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
async getToken(forceRefresh = false) {
|
|
90
|
+
if (this.accessToken && this.tokenExpiry && this.tokenExpiry > Date.now() && !forceRefresh) {
|
|
91
|
+
return this.accessToken;
|
|
92
|
+
}
|
|
93
|
+
const accounts = await this.msalApp.getTokenCache().getAllAccounts();
|
|
94
|
+
if (accounts.length > 0) {
|
|
95
|
+
const silentRequest = {
|
|
96
|
+
account: accounts[0],
|
|
97
|
+
scopes: this.scopes,
|
|
98
|
+
};
|
|
99
|
+
try {
|
|
100
|
+
const response = await this.msalApp.acquireTokenSilent(silentRequest);
|
|
101
|
+
this.accessToken = response.accessToken;
|
|
102
|
+
this.tokenExpiry = response.expiresOn ? new Date(response.expiresOn).getTime() : null;
|
|
103
|
+
return this.accessToken;
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
logger.info('Silent token acquisition failed, using device code flow');
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
throw new Error('No valid token found');
|
|
110
|
+
}
|
|
111
|
+
async acquireTokenByDeviceCode(hack) {
|
|
112
|
+
const deviceCodeRequest = {
|
|
113
|
+
scopes: this.scopes,
|
|
114
|
+
deviceCodeCallback: (response) => {
|
|
115
|
+
const text = ['\n', response.message, '\n'].join('');
|
|
116
|
+
if (hack) {
|
|
117
|
+
hack(text + 'After login run the "verify login" command');
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
console.log(text);
|
|
121
|
+
}
|
|
122
|
+
logger.info('Device code login initiated');
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
try {
|
|
126
|
+
logger.info('Requesting device code...');
|
|
127
|
+
const response = await this.msalApp.acquireTokenByDeviceCode(deviceCodeRequest);
|
|
128
|
+
logger.info('Device code login successful');
|
|
129
|
+
this.accessToken = response?.accessToken || null;
|
|
130
|
+
this.tokenExpiry = response?.expiresOn ? new Date(response.expiresOn).getTime() : null;
|
|
131
|
+
await this.saveTokenCache();
|
|
132
|
+
return this.accessToken;
|
|
133
|
+
}
|
|
134
|
+
catch (error) {
|
|
135
|
+
logger.error(`Error in device code flow: ${error.message}`);
|
|
136
|
+
throw error;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
async testLogin() {
|
|
140
|
+
try {
|
|
141
|
+
logger.info('Testing login...');
|
|
142
|
+
const token = await this.getToken();
|
|
143
|
+
if (!token) {
|
|
144
|
+
logger.error('Login test failed - no token received');
|
|
145
|
+
return {
|
|
146
|
+
success: false,
|
|
147
|
+
message: 'Login failed - no token received',
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
logger.info('Token retrieved successfully, testing Graph API access...');
|
|
151
|
+
try {
|
|
152
|
+
const response = await fetch('https://graph.microsoft.com/v1.0/me', {
|
|
153
|
+
headers: {
|
|
154
|
+
Authorization: `Bearer ${token}`,
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
if (response.ok) {
|
|
158
|
+
const userData = await response.json();
|
|
159
|
+
logger.info('Graph API user data fetch successful');
|
|
160
|
+
return {
|
|
161
|
+
success: true,
|
|
162
|
+
message: 'Login successful',
|
|
163
|
+
userData: {
|
|
164
|
+
displayName: userData.displayName,
|
|
165
|
+
userPrincipalName: userData.userPrincipalName,
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
const errorText = await response.text();
|
|
171
|
+
logger.error(`Graph API user data fetch failed: ${response.status} - ${errorText}`);
|
|
172
|
+
return {
|
|
173
|
+
success: false,
|
|
174
|
+
message: `Login successful but Graph API access failed: ${response.status}`,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
catch (graphError) {
|
|
179
|
+
logger.error(`Error fetching user data: ${graphError.message}`);
|
|
180
|
+
return {
|
|
181
|
+
success: false,
|
|
182
|
+
message: `Login successful but Graph API access failed: ${graphError.message}`,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
catch (error) {
|
|
187
|
+
logger.error(`Login test failed: ${error.message}`);
|
|
188
|
+
return {
|
|
189
|
+
success: false,
|
|
190
|
+
message: `Login failed: ${error.message}`,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
async logout() {
|
|
195
|
+
try {
|
|
196
|
+
const accounts = await this.msalApp.getTokenCache().getAllAccounts();
|
|
197
|
+
for (const account of accounts) {
|
|
198
|
+
await this.msalApp.getTokenCache().removeAccount(account);
|
|
199
|
+
}
|
|
200
|
+
this.accessToken = null;
|
|
201
|
+
this.tokenExpiry = null;
|
|
202
|
+
try {
|
|
203
|
+
await keytar.deletePassword(SERVICE_NAME, TOKEN_CACHE_ACCOUNT);
|
|
204
|
+
}
|
|
205
|
+
catch (keytarError) {
|
|
206
|
+
logger.warn(`Keychain deletion failed: ${keytarError.message}`);
|
|
207
|
+
}
|
|
208
|
+
if (fs.existsSync(FALLBACK_PATH)) {
|
|
209
|
+
fs.unlinkSync(FALLBACK_PATH);
|
|
210
|
+
}
|
|
211
|
+
return true;
|
|
212
|
+
}
|
|
213
|
+
catch (error) {
|
|
214
|
+
logger.error(`Error during logout: ${error.message}`);
|
|
215
|
+
throw error;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
export default AuthManager;
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { readFileSync } from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
const packageJsonPath = path.join(__dirname, '..', 'package.json');
|
|
7
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
|
|
8
|
+
const version = packageJson.version;
|
|
9
|
+
const program = new Command();
|
|
10
|
+
program
|
|
11
|
+
.name('ms-365-mcp-server')
|
|
12
|
+
.description('Microsoft 365 MCP Server')
|
|
13
|
+
.version(version)
|
|
14
|
+
.option('-v', 'Enable verbose logging')
|
|
15
|
+
.option('--login', 'Login using device code flow')
|
|
16
|
+
.option('--logout', 'Log out and clear saved credentials')
|
|
17
|
+
.option('--verify-login', 'Verify login without starting the server');
|
|
18
|
+
export function parseArgs() {
|
|
19
|
+
program.parse();
|
|
20
|
+
return program.opts();
|
|
21
|
+
}
|