@dashflow/ms365-mcp-server 1.0.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/dist/index.js ADDED
@@ -0,0 +1,76 @@
1
+ #!/usr/bin/env node
2
+ import "dotenv/config";
3
+ import { parseArgs } from "./cli.js";
4
+ import logger from "./logger.js";
5
+ import AuthManager, { buildScopesFromEndpoints } from "./auth.js";
6
+ import MicrosoftGraphServer from "./server.js";
7
+ import { version } from "./version.js";
8
+ async function main() {
9
+ try {
10
+ const args = parseArgs();
11
+ const includeWorkScopes = args.orgMode || false;
12
+ if (includeWorkScopes) {
13
+ logger.info("Organization mode enabled - including work account scopes");
14
+ }
15
+ const scopes = buildScopesFromEndpoints(includeWorkScopes, args.enabledTools);
16
+ const authManager = await AuthManager.create(scopes);
17
+ await authManager.loadTokenCache();
18
+ if (args.login) {
19
+ await authManager.acquireTokenByDeviceCode();
20
+ logger.info("Login completed, testing connection with Graph API...");
21
+ const result = await authManager.testLogin();
22
+ console.log(JSON.stringify(result));
23
+ process.exit(0);
24
+ }
25
+ if (args.verifyLogin) {
26
+ logger.info("Verifying login...");
27
+ const result = await authManager.testLogin();
28
+ console.log(JSON.stringify(result));
29
+ process.exit(0);
30
+ }
31
+ if (args.logout) {
32
+ await authManager.logout();
33
+ console.log(JSON.stringify({ message: "Logged out successfully" }));
34
+ process.exit(0);
35
+ }
36
+ if (args.listAccounts) {
37
+ const accounts = await authManager.listAccounts();
38
+ const selectedAccountId = authManager.getSelectedAccountId();
39
+ const result = accounts.map((account) => ({
40
+ id: account.homeAccountId,
41
+ username: account.username,
42
+ name: account.name,
43
+ selected: account.homeAccountId === selectedAccountId
44
+ }));
45
+ console.log(JSON.stringify({ accounts: result }));
46
+ process.exit(0);
47
+ }
48
+ if (args.selectAccount) {
49
+ const success = await authManager.selectAccount(args.selectAccount);
50
+ if (success) {
51
+ console.log(JSON.stringify({ message: `Selected account: ${args.selectAccount}` }));
52
+ } else {
53
+ console.log(JSON.stringify({ error: `Account not found: ${args.selectAccount}` }));
54
+ process.exit(1);
55
+ }
56
+ process.exit(0);
57
+ }
58
+ if (args.removeAccount) {
59
+ const success = await authManager.removeAccount(args.removeAccount);
60
+ if (success) {
61
+ console.log(JSON.stringify({ message: `Removed account: ${args.removeAccount}` }));
62
+ } else {
63
+ console.log(JSON.stringify({ error: `Account not found: ${args.removeAccount}` }));
64
+ process.exit(1);
65
+ }
66
+ process.exit(0);
67
+ }
68
+ const server = new MicrosoftGraphServer(authManager, args);
69
+ await server.initialize(version);
70
+ await server.start();
71
+ } catch (error) {
72
+ logger.error(`Startup error: ${error}`);
73
+ process.exit(1);
74
+ }
75
+ }
76
+ main();
@@ -0,0 +1,73 @@
1
+ import logger from "../logger.js";
2
+ import { getCloudEndpoints } from "../cloud-config.js";
3
+ const microsoftBearerTokenAuthMiddleware = (req, res, next) => {
4
+ const authHeader = req.headers.authorization;
5
+ if (!authHeader || !authHeader.startsWith("Bearer ")) {
6
+ res.status(401).json({ error: "Missing or invalid access token" });
7
+ return;
8
+ }
9
+ const accessToken = authHeader.substring(7);
10
+ const refreshToken = req.headers["x-microsoft-refresh-token"] || "";
11
+ req.microsoftAuth = {
12
+ accessToken,
13
+ refreshToken
14
+ };
15
+ next();
16
+ };
17
+ async function exchangeCodeForToken(code, redirectUri, clientId, clientSecret, tenantId = "common", codeVerifier, cloudType = "global") {
18
+ const cloudEndpoints = getCloudEndpoints(cloudType);
19
+ const params = new URLSearchParams({
20
+ grant_type: "authorization_code",
21
+ code,
22
+ redirect_uri: redirectUri,
23
+ client_id: clientId
24
+ });
25
+ if (clientSecret) {
26
+ params.append("client_secret", clientSecret);
27
+ }
28
+ if (codeVerifier) {
29
+ params.append("code_verifier", codeVerifier);
30
+ }
31
+ const response = await fetch(`${cloudEndpoints.authority}/${tenantId}/oauth2/v2.0/token`, {
32
+ method: "POST",
33
+ headers: {
34
+ "Content-Type": "application/x-www-form-urlencoded"
35
+ },
36
+ body: params
37
+ });
38
+ if (!response.ok) {
39
+ const error = await response.text();
40
+ logger.error(`Failed to exchange code for token: ${error}`);
41
+ throw new Error(`Failed to exchange code for token: ${error}`);
42
+ }
43
+ return response.json();
44
+ }
45
+ async function refreshAccessToken(refreshToken, clientId, clientSecret, tenantId = "common", cloudType = "global") {
46
+ const cloudEndpoints = getCloudEndpoints(cloudType);
47
+ const params = new URLSearchParams({
48
+ grant_type: "refresh_token",
49
+ refresh_token: refreshToken,
50
+ client_id: clientId
51
+ });
52
+ if (clientSecret) {
53
+ params.append("client_secret", clientSecret);
54
+ }
55
+ const response = await fetch(`${cloudEndpoints.authority}/${tenantId}/oauth2/v2.0/token`, {
56
+ method: "POST",
57
+ headers: {
58
+ "Content-Type": "application/x-www-form-urlencoded"
59
+ },
60
+ body: params
61
+ });
62
+ if (!response.ok) {
63
+ const error = await response.text();
64
+ logger.error(`Failed to refresh token: ${error}`);
65
+ throw new Error(`Failed to refresh token: ${error}`);
66
+ }
67
+ return response.json();
68
+ }
69
+ export {
70
+ exchangeCodeForToken,
71
+ microsoftBearerTokenAuthMiddleware,
72
+ refreshAccessToken
73
+ };
package/dist/logger.js ADDED
@@ -0,0 +1,42 @@
1
+ import winston from "winston";
2
+ import path from "path";
3
+ import { fileURLToPath } from "url";
4
+ import fs from "fs";
5
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
6
+ const logsDir = path.join(__dirname, "..", "logs");
7
+ if (!fs.existsSync(logsDir)) {
8
+ fs.mkdirSync(logsDir);
9
+ }
10
+ const logger = winston.createLogger({
11
+ level: process.env.LOG_LEVEL || "info",
12
+ format: winston.format.combine(
13
+ winston.format.timestamp({
14
+ format: "YYYY-MM-DD HH:mm:ss"
15
+ }),
16
+ winston.format.printf(({ level, message, timestamp }) => {
17
+ return `${timestamp} ${level.toUpperCase()}: ${message}`;
18
+ })
19
+ ),
20
+ transports: [
21
+ new winston.transports.File({
22
+ filename: path.join(logsDir, "error.log"),
23
+ level: "error"
24
+ }),
25
+ new winston.transports.File({
26
+ filename: path.join(logsDir, "mcp-server.log")
27
+ })
28
+ ]
29
+ });
30
+ const enableConsoleLogging = () => {
31
+ logger.add(
32
+ new winston.transports.Console({
33
+ format: winston.format.combine(winston.format.colorize(), winston.format.simple()),
34
+ silent: process.env.SILENT === "true" || process.env.SILENT === "1"
35
+ })
36
+ );
37
+ };
38
+ var logger_default = logger;
39
+ export {
40
+ logger_default as default,
41
+ enableConsoleLogging
42
+ };
@@ -0,0 +1,51 @@
1
+ import { ProxyOAuthServerProvider } from "@modelcontextprotocol/sdk/server/auth/providers/proxyProvider.js";
2
+ import logger from "./logger.js";
3
+ import { getCloudEndpoints } from "./cloud-config.js";
4
+ class MicrosoftOAuthProvider extends ProxyOAuthServerProvider {
5
+ constructor(authManager, secrets) {
6
+ const tenantId = secrets.tenantId || "common";
7
+ const clientId = secrets.clientId;
8
+ const cloudEndpoints = getCloudEndpoints(secrets.cloudType);
9
+ super({
10
+ endpoints: {
11
+ authorizationUrl: `${cloudEndpoints.authority}/${tenantId}/oauth2/v2.0/authorize`,
12
+ tokenUrl: `${cloudEndpoints.authority}/${tenantId}/oauth2/v2.0/token`,
13
+ revocationUrl: `${cloudEndpoints.authority}/${tenantId}/oauth2/v2.0/logout`
14
+ },
15
+ verifyAccessToken: async (token) => {
16
+ try {
17
+ const response = await fetch(`${cloudEndpoints.graphApi}/v1.0/me`, {
18
+ headers: {
19
+ Authorization: `Bearer ${token}`
20
+ }
21
+ });
22
+ if (response.ok) {
23
+ const userData = await response.json();
24
+ logger.info(`OAuth token verified for user: ${userData.userPrincipalName}`);
25
+ await authManager.setOAuthToken(token);
26
+ return {
27
+ token,
28
+ clientId,
29
+ scopes: []
30
+ };
31
+ } else {
32
+ throw new Error(`Token verification failed: ${response.status}`);
33
+ }
34
+ } catch (error) {
35
+ logger.error(`OAuth token verification error: ${error}`);
36
+ throw error;
37
+ }
38
+ },
39
+ getClient: async (client_id) => {
40
+ return {
41
+ client_id,
42
+ redirect_uris: ["http://localhost:3000/callback"]
43
+ };
44
+ }
45
+ });
46
+ this.authManager = authManager;
47
+ }
48
+ }
49
+ export {
50
+ MicrosoftOAuthProvider
51
+ };
@@ -0,0 +1,9 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+ const requestContext = new AsyncLocalStorage();
3
+ function getRequestTokens() {
4
+ return requestContext.getStore();
5
+ }
6
+ export {
7
+ getRequestTokens,
8
+ requestContext
9
+ };
@@ -0,0 +1,68 @@
1
+ import logger from "./logger.js";
2
+ import { parseCloudType, getDefaultClientId } from "./cloud-config.js";
3
+ class EnvironmentSecretsProvider {
4
+ async getSecrets() {
5
+ const cloudType = parseCloudType(process.env.MS365_MCP_CLOUD_TYPE);
6
+ return {
7
+ clientId: process.env.MS365_MCP_CLIENT_ID || getDefaultClientId(cloudType),
8
+ tenantId: process.env.MS365_MCP_TENANT_ID || "common",
9
+ clientSecret: process.env.MS365_MCP_CLIENT_SECRET,
10
+ cloudType
11
+ };
12
+ }
13
+ }
14
+ class KeyVaultSecretsProvider {
15
+ constructor(vaultUrl) {
16
+ this.vaultUrl = vaultUrl;
17
+ }
18
+ async getSecrets() {
19
+ const { DefaultAzureCredential } = await import("@azure/identity");
20
+ const { SecretClient } = await import("@azure/keyvault-secrets");
21
+ const credential = new DefaultAzureCredential();
22
+ const client = new SecretClient(this.vaultUrl, credential);
23
+ logger.info(`Fetching secrets from Key Vault: ${this.vaultUrl}`);
24
+ const [clientIdSecret, tenantIdSecret, clientSecretResult, cloudTypeResult] = await Promise.all(
25
+ [
26
+ client.getSecret("ms365-mcp-client-id"),
27
+ client.getSecret("ms365-mcp-tenant-id").catch(() => null),
28
+ client.getSecret("ms365-mcp-client-secret").catch(() => null),
29
+ client.getSecret("ms365-mcp-cloud-type").catch(() => null)
30
+ ]
31
+ );
32
+ if (!clientIdSecret.value) {
33
+ throw new Error("Required secret ms365-mcp-client-id not found in Key Vault");
34
+ }
35
+ logger.info("Successfully retrieved secrets from Key Vault");
36
+ return {
37
+ clientId: clientIdSecret.value,
38
+ tenantId: tenantIdSecret?.value || "common",
39
+ clientSecret: clientSecretResult?.value,
40
+ cloudType: parseCloudType(cloudTypeResult?.value)
41
+ };
42
+ }
43
+ }
44
+ function createSecretsProvider() {
45
+ const vaultUrl = process.env.MS365_MCP_KEYVAULT_URL;
46
+ if (vaultUrl) {
47
+ logger.info("Key Vault URL configured, using Azure Key Vault for secrets");
48
+ return new KeyVaultSecretsProvider(vaultUrl);
49
+ }
50
+ logger.info("Using environment variables for secrets");
51
+ return new EnvironmentSecretsProvider();
52
+ }
53
+ let cachedSecrets = null;
54
+ async function getSecrets() {
55
+ if (cachedSecrets) {
56
+ return cachedSecrets;
57
+ }
58
+ const provider = createSecretsProvider();
59
+ cachedSecrets = await provider.getSecrets();
60
+ return cachedSecrets;
61
+ }
62
+ function clearSecretsCache() {
63
+ cachedSecrets = null;
64
+ }
65
+ export {
66
+ clearSecretsCache,
67
+ getSecrets
68
+ };