@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/server.js ADDED
@@ -0,0 +1,387 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
4
+ import { mcpAuthRouter } from "@modelcontextprotocol/sdk/server/auth/router.js";
5
+ import express from "express";
6
+ import logger, { enableConsoleLogging } from "./logger.js";
7
+ import { registerAuthTools } from "./auth-tools.js";
8
+ import { registerGraphTools, registerDiscoveryTools } from "./graph-tools.js";
9
+ import GraphClient from "./graph-client.js";
10
+ import { buildScopesFromEndpoints } from "./auth.js";
11
+ import { MicrosoftOAuthProvider } from "./oauth-provider.js";
12
+ import {
13
+ exchangeCodeForToken,
14
+ microsoftBearerTokenAuthMiddleware,
15
+ refreshAccessToken
16
+ } from "./lib/microsoft-auth.js";
17
+ import { getSecrets } from "./secrets.js";
18
+ import { getCloudEndpoints } from "./cloud-config.js";
19
+ import { requestContext } from "./request-context.js";
20
+ function parseHttpOption(httpOption) {
21
+ if (typeof httpOption === "boolean") {
22
+ return { host: void 0, port: 3e3 };
23
+ }
24
+ const httpString = httpOption.trim();
25
+ if (httpString.includes(":")) {
26
+ const [hostPart, portPart] = httpString.split(":");
27
+ const host = hostPart || void 0;
28
+ const port2 = parseInt(portPart) || 3e3;
29
+ return { host, port: port2 };
30
+ }
31
+ const port = parseInt(httpString) || 3e3;
32
+ return { host: void 0, port };
33
+ }
34
+ class MicrosoftGraphServer {
35
+ constructor(authManager, options = {}) {
36
+ this.authManager = authManager;
37
+ this.options = options;
38
+ this.graphClient = null;
39
+ this.server = null;
40
+ this.secrets = null;
41
+ }
42
+ async initialize(version) {
43
+ this.secrets = await getSecrets();
44
+ const outputFormat = this.options.toon ? "toon" : "json";
45
+ this.graphClient = new GraphClient(this.authManager, this.secrets, outputFormat);
46
+ this.server = new McpServer({
47
+ name: "Microsoft365MCP",
48
+ version
49
+ });
50
+ const shouldRegisterAuthTools = !this.options.http || this.options.enableAuthTools;
51
+ if (shouldRegisterAuthTools) {
52
+ registerAuthTools(this.server, this.authManager);
53
+ }
54
+ if (this.options.discovery) {
55
+ logger.info("Discovery mode enabled (experimental) - registering discovery tool only");
56
+ registerDiscoveryTools(
57
+ this.server,
58
+ this.graphClient,
59
+ this.options.readOnly,
60
+ this.options.orgMode
61
+ );
62
+ } else {
63
+ registerGraphTools(
64
+ this.server,
65
+ this.graphClient,
66
+ this.options.readOnly,
67
+ this.options.enabledTools,
68
+ this.options.orgMode
69
+ );
70
+ }
71
+ }
72
+ async start() {
73
+ if (this.options.v) {
74
+ enableConsoleLogging();
75
+ }
76
+ logger.info("Microsoft 365 MCP Server starting...");
77
+ logger.info("Secrets Check:", {
78
+ CLIENT_ID: this.secrets?.clientId ? `${this.secrets.clientId.substring(0, 8)}...` : "NOT SET",
79
+ CLIENT_SECRET: this.secrets?.clientSecret ? "SET" : "NOT SET",
80
+ TENANT_ID: this.secrets?.tenantId || "NOT SET",
81
+ NODE_ENV: process.env.NODE_ENV || "NOT SET"
82
+ });
83
+ if (this.options.readOnly) {
84
+ logger.info("Server running in READ-ONLY mode. Write operations are disabled.");
85
+ }
86
+ if (this.options.http) {
87
+ const { host, port } = parseHttpOption(this.options.http);
88
+ const app = express();
89
+ app.set("trust proxy", true);
90
+ app.use(express.json());
91
+ app.use(express.urlencoded({ extended: true }));
92
+ const corsOrigin = process.env.MS365_MCP_CORS_ORIGIN || "*";
93
+ app.use((req, res, next) => {
94
+ res.header("Access-Control-Allow-Origin", corsOrigin);
95
+ res.header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
96
+ res.header(
97
+ "Access-Control-Allow-Headers",
98
+ "Origin, X-Requested-With, Content-Type, Accept, Authorization, mcp-protocol-version"
99
+ );
100
+ if (req.method === "OPTIONS") {
101
+ res.sendStatus(200);
102
+ return;
103
+ }
104
+ next();
105
+ });
106
+ const oauthProvider = new MicrosoftOAuthProvider(this.authManager, this.secrets);
107
+ app.get("/.well-known/oauth-authorization-server", async (req, res) => {
108
+ const protocol = req.secure ? "https" : "http";
109
+ const url = new URL(`${protocol}://${req.get("host")}`);
110
+ const scopes = buildScopesFromEndpoints(this.options.orgMode, this.options.enabledTools);
111
+ const metadata = {
112
+ issuer: url.origin,
113
+ authorization_endpoint: `${url.origin}/authorize`,
114
+ token_endpoint: `${url.origin}/token`,
115
+ response_types_supported: ["code"],
116
+ response_modes_supported: ["query"],
117
+ grant_types_supported: ["authorization_code", "refresh_token"],
118
+ token_endpoint_auth_methods_supported: ["none"],
119
+ code_challenge_methods_supported: ["S256"],
120
+ scopes_supported: scopes
121
+ };
122
+ if (this.options.enableDynamicRegistration) {
123
+ metadata.registration_endpoint = `${url.origin}/register`;
124
+ }
125
+ res.json(metadata);
126
+ });
127
+ app.get("/.well-known/oauth-protected-resource", async (req, res) => {
128
+ const protocol = req.secure ? "https" : "http";
129
+ const url = new URL(`${protocol}://${req.get("host")}`);
130
+ const scopes = buildScopesFromEndpoints(this.options.orgMode, this.options.enabledTools);
131
+ res.json({
132
+ resource: `${url.origin}/mcp`,
133
+ authorization_servers: [url.origin],
134
+ scopes_supported: scopes,
135
+ bearer_methods_supported: ["header"],
136
+ resource_documentation: `${url.origin}`
137
+ });
138
+ });
139
+ if (this.options.enableDynamicRegistration) {
140
+ app.post("/register", async (req, res) => {
141
+ const body = req.body;
142
+ logger.info("Client registration request", { body });
143
+ const clientId = `mcp-client-${Date.now()}`;
144
+ res.status(201).json({
145
+ client_id: clientId,
146
+ client_id_issued_at: Math.floor(Date.now() / 1e3),
147
+ redirect_uris: body.redirect_uris || [],
148
+ grant_types: body.grant_types || ["authorization_code", "refresh_token"],
149
+ response_types: body.response_types || ["code"],
150
+ token_endpoint_auth_method: body.token_endpoint_auth_method || "none",
151
+ client_name: body.client_name || "MCP Client"
152
+ });
153
+ });
154
+ }
155
+ app.get("/authorize", async (req, res) => {
156
+ const url = new URL(req.url, `${req.protocol}://${req.get("host")}`);
157
+ const tenantId = this.secrets?.tenantId || "common";
158
+ const clientId = this.secrets.clientId;
159
+ const cloudEndpoints = getCloudEndpoints(this.secrets.cloudType);
160
+ const microsoftAuthUrl = new URL(
161
+ `${cloudEndpoints.authority}/${tenantId}/oauth2/v2.0/authorize`
162
+ );
163
+ const allowedParams = [
164
+ "response_type",
165
+ "redirect_uri",
166
+ "scope",
167
+ "state",
168
+ "response_mode",
169
+ "code_challenge",
170
+ "code_challenge_method",
171
+ "prompt",
172
+ "login_hint",
173
+ "domain_hint"
174
+ ];
175
+ allowedParams.forEach((param) => {
176
+ const value = url.searchParams.get(param);
177
+ if (value) {
178
+ microsoftAuthUrl.searchParams.set(param, value);
179
+ }
180
+ });
181
+ microsoftAuthUrl.searchParams.set("client_id", clientId);
182
+ if (!microsoftAuthUrl.searchParams.get("scope")) {
183
+ microsoftAuthUrl.searchParams.set("scope", "User.Read Files.Read Mail.Read");
184
+ }
185
+ res.redirect(microsoftAuthUrl.toString());
186
+ });
187
+ app.post("/token", async (req, res) => {
188
+ try {
189
+ logger.info("Token endpoint called", {
190
+ method: req.method,
191
+ url: req.url,
192
+ contentType: req.get("Content-Type"),
193
+ grant_type: req.body?.grant_type
194
+ });
195
+ const body = req.body;
196
+ if (!body) {
197
+ logger.error("Token endpoint: Request body is undefined");
198
+ res.status(400).json({
199
+ error: "invalid_request",
200
+ error_description: "Request body is required"
201
+ });
202
+ return;
203
+ }
204
+ if (!body.grant_type) {
205
+ logger.error("Token endpoint: grant_type is missing", { body });
206
+ res.status(400).json({
207
+ error: "invalid_request",
208
+ error_description: "grant_type parameter is required"
209
+ });
210
+ return;
211
+ }
212
+ if (body.grant_type === "authorization_code") {
213
+ const tenantId = this.secrets?.tenantId || "common";
214
+ const clientId = this.secrets.clientId;
215
+ const clientSecret = this.secrets?.clientSecret;
216
+ logger.info("Token endpoint: authorization_code exchange", {
217
+ redirect_uri: body.redirect_uri,
218
+ has_code: !!body.code,
219
+ has_code_verifier: !!body.code_verifier,
220
+ clientId,
221
+ tenantId,
222
+ hasClientSecret: !!clientSecret
223
+ });
224
+ const result = await exchangeCodeForToken(
225
+ body.code,
226
+ body.redirect_uri,
227
+ clientId,
228
+ clientSecret,
229
+ tenantId,
230
+ body.code_verifier,
231
+ this.secrets.cloudType
232
+ );
233
+ res.json(result);
234
+ } else if (body.grant_type === "refresh_token") {
235
+ const tenantId = this.secrets?.tenantId || "common";
236
+ const clientId = this.secrets.clientId;
237
+ const clientSecret = this.secrets?.clientSecret;
238
+ if (clientSecret) {
239
+ logger.info("Refresh endpoint: Using confidential client with client_secret");
240
+ } else {
241
+ logger.info("Refresh endpoint: Using public client without client_secret");
242
+ }
243
+ const result = await refreshAccessToken(
244
+ body.refresh_token,
245
+ clientId,
246
+ clientSecret,
247
+ tenantId,
248
+ this.secrets.cloudType
249
+ );
250
+ res.json(result);
251
+ } else {
252
+ res.status(400).json({
253
+ error: "unsupported_grant_type",
254
+ error_description: `Grant type '${body.grant_type}' is not supported`
255
+ });
256
+ }
257
+ } catch (error) {
258
+ logger.error("Token endpoint error:", error);
259
+ res.status(500).json({
260
+ error: "server_error",
261
+ error_description: "Internal server error during token exchange"
262
+ });
263
+ }
264
+ });
265
+ app.use(
266
+ mcpAuthRouter({
267
+ provider: oauthProvider,
268
+ issuerUrl: new URL(`http://localhost:${port}`)
269
+ })
270
+ );
271
+ app.get(
272
+ "/mcp",
273
+ microsoftBearerTokenAuthMiddleware,
274
+ async (req, res) => {
275
+ const handler = async () => {
276
+ const transport = new StreamableHTTPServerTransport({
277
+ sessionIdGenerator: void 0
278
+ // Stateless mode
279
+ });
280
+ res.on("close", () => {
281
+ transport.close();
282
+ });
283
+ await this.server.connect(transport);
284
+ await transport.handleRequest(req, res, void 0);
285
+ };
286
+ try {
287
+ if (req.microsoftAuth) {
288
+ await requestContext.run(
289
+ {
290
+ accessToken: req.microsoftAuth.accessToken,
291
+ refreshToken: req.microsoftAuth.refreshToken
292
+ },
293
+ handler
294
+ );
295
+ } else {
296
+ await handler();
297
+ }
298
+ } catch (error) {
299
+ logger.error("Error handling MCP GET request:", error);
300
+ if (!res.headersSent) {
301
+ res.status(500).json({
302
+ jsonrpc: "2.0",
303
+ error: {
304
+ code: -32603,
305
+ message: "Internal server error"
306
+ },
307
+ id: null
308
+ });
309
+ }
310
+ }
311
+ }
312
+ );
313
+ app.post(
314
+ "/mcp",
315
+ microsoftBearerTokenAuthMiddleware,
316
+ async (req, res) => {
317
+ const handler = async () => {
318
+ const transport = new StreamableHTTPServerTransport({
319
+ sessionIdGenerator: void 0
320
+ // Stateless mode
321
+ });
322
+ res.on("close", () => {
323
+ transport.close();
324
+ });
325
+ await this.server.connect(transport);
326
+ await transport.handleRequest(req, res, req.body);
327
+ };
328
+ try {
329
+ if (req.microsoftAuth) {
330
+ await requestContext.run(
331
+ {
332
+ accessToken: req.microsoftAuth.accessToken,
333
+ refreshToken: req.microsoftAuth.refreshToken
334
+ },
335
+ handler
336
+ );
337
+ } else {
338
+ await handler();
339
+ }
340
+ } catch (error) {
341
+ logger.error("Error handling MCP POST request:", error);
342
+ if (!res.headersSent) {
343
+ res.status(500).json({
344
+ jsonrpc: "2.0",
345
+ error: {
346
+ code: -32603,
347
+ message: "Internal server error"
348
+ },
349
+ id: null
350
+ });
351
+ }
352
+ }
353
+ }
354
+ );
355
+ app.get("/", (req, res) => {
356
+ res.send("Microsoft 365 MCP Server is running");
357
+ });
358
+ if (host) {
359
+ app.listen(port, host, () => {
360
+ logger.info(`Server listening on ${host}:${port}`);
361
+ logger.info(` - MCP endpoint: http://${host}:${port}/mcp`);
362
+ logger.info(` - OAuth endpoints: http://${host}:${port}/auth/*`);
363
+ logger.info(
364
+ ` - OAuth discovery: http://${host}:${port}/.well-known/oauth-authorization-server`
365
+ );
366
+ });
367
+ } else {
368
+ app.listen(port, () => {
369
+ logger.info(`Server listening on all interfaces (0.0.0.0:${port})`);
370
+ logger.info(` - MCP endpoint: http://localhost:${port}/mcp`);
371
+ logger.info(` - OAuth endpoints: http://localhost:${port}/auth/*`);
372
+ logger.info(
373
+ ` - OAuth discovery: http://localhost:${port}/.well-known/oauth-authorization-server`
374
+ );
375
+ });
376
+ }
377
+ } else {
378
+ const transport = new StdioServerTransport();
379
+ await this.server.connect(transport);
380
+ logger.info("Server connected to stdio transport");
381
+ }
382
+ }
383
+ }
384
+ var server_default = MicrosoftGraphServer;
385
+ export {
386
+ server_default as default
387
+ };
@@ -0,0 +1,93 @@
1
+ const TOOL_CATEGORIES = {
2
+ mail: {
3
+ name: "mail",
4
+ pattern: /mail|attachment|draft/i,
5
+ description: "Email operations (read, send, manage folders, attachments)"
6
+ },
7
+ calendar: {
8
+ name: "calendar",
9
+ pattern: /calendar|event/i,
10
+ description: "Calendar and event management"
11
+ },
12
+ files: {
13
+ name: "files",
14
+ pattern: /drive|file|upload|download|folder|item/i,
15
+ description: "OneDrive file and folder operations"
16
+ },
17
+ personal: {
18
+ name: "personal",
19
+ pattern: /mail|calendar|drive|contact|todo|onenote|attachment|draft|event|file|folder/i,
20
+ description: "Personal productivity tools (mail, calendar, files, contacts, tasks, notes)"
21
+ },
22
+ work: {
23
+ name: "work",
24
+ pattern: /team|channel|chat|sharepoint|planner|site|list|shared/i,
25
+ description: "Organization/work tools (Teams, SharePoint, shared mailboxes)",
26
+ requiresOrgMode: true
27
+ },
28
+ excel: {
29
+ name: "excel",
30
+ pattern: /excel|worksheet|workbook|range|chart/i,
31
+ description: "Excel spreadsheet operations"
32
+ },
33
+ contacts: {
34
+ name: "contacts",
35
+ pattern: /contact/i,
36
+ description: "Outlook contacts management"
37
+ },
38
+ tasks: {
39
+ name: "tasks",
40
+ pattern: /todo|planner|task/i,
41
+ description: "Task and planning tools (To Do, Planner)"
42
+ },
43
+ onenote: {
44
+ name: "onenote",
45
+ pattern: /onenote|notebook|section|page/i,
46
+ description: "OneNote notebook operations"
47
+ },
48
+ search: {
49
+ name: "search",
50
+ pattern: /search|query/i,
51
+ description: "Microsoft Search capabilities"
52
+ },
53
+ users: {
54
+ name: "users",
55
+ pattern: /user|list-users/i,
56
+ description: "User directory access",
57
+ requiresOrgMode: true
58
+ },
59
+ all: {
60
+ name: "all",
61
+ pattern: /.*/,
62
+ description: "All available tools"
63
+ }
64
+ };
65
+ function getCombinedPresetPattern(presets) {
66
+ const patterns = presets.map((preset) => {
67
+ const category = TOOL_CATEGORIES[preset];
68
+ if (!category) {
69
+ throw new Error(
70
+ `Unknown preset: ${preset}. Available presets: ${Object.keys(TOOL_CATEGORIES).join(", ")}`
71
+ );
72
+ }
73
+ return category.pattern.source;
74
+ });
75
+ return patterns.join("|");
76
+ }
77
+ function listPresets() {
78
+ return Object.values(TOOL_CATEGORIES).map((category) => ({
79
+ name: category.name,
80
+ description: category.description,
81
+ requiresOrgMode: category.requiresOrgMode
82
+ }));
83
+ }
84
+ function presetRequiresOrgMode(preset) {
85
+ const category = TOOL_CATEGORIES[preset];
86
+ return category?.requiresOrgMode || false;
87
+ }
88
+ export {
89
+ TOOL_CATEGORIES,
90
+ getCombinedPresetPattern,
91
+ listPresets,
92
+ presetRequiresOrgMode
93
+ };
@@ -0,0 +1,10 @@
1
+ import { readFileSync } from "fs";
2
+ import path from "path";
3
+ import { fileURLToPath } from "url";
4
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
5
+ const packageJsonPath = path.join(__dirname, "..", "package.json");
6
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
7
+ const version = packageJson.version;
8
+ export {
9
+ version
10
+ };
@@ -0,0 +1,43 @@
1
+ import js from '@eslint/js';
2
+ import globals from 'globals';
3
+ import tseslint from '@typescript-eslint/eslint-plugin';
4
+ import tsparser from '@typescript-eslint/parser';
5
+
6
+ export default [
7
+ js.configs.recommended,
8
+ {
9
+ files: ['**/*.{ts,tsx,js,mjs}'],
10
+ languageOptions: {
11
+ parser: tsparser,
12
+ parserOptions: {
13
+ ecmaVersion: 2022,
14
+ sourceType: 'module',
15
+ },
16
+ globals: {
17
+ ...globals.node,
18
+ ...globals.vitest,
19
+ ...globals.jest,
20
+ fs: 'readonly',
21
+ },
22
+ },
23
+ plugins: {
24
+ '@typescript-eslint': tseslint,
25
+ },
26
+ rules: {
27
+ ...tseslint.configs.recommended.rules,
28
+ '@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }],
29
+ '@typescript-eslint/no-explicit-any': 'warn',
30
+ 'no-console': 'off',
31
+ },
32
+ },
33
+ {
34
+ ignores: [
35
+ 'node_modules/**',
36
+ 'dist/**',
37
+ 'coverage/**',
38
+ 'bin/**',
39
+ 'src/generated/**',
40
+ '.venv/**',
41
+ ],
42
+ },
43
+ ];
package/glama.json ADDED
@@ -0,0 +1,4 @@
1
+ {
2
+ "$schema": "https://glama.ai/mcp/schemas/server.json",
3
+ "maintainers": ["eirikb"]
4
+ }
package/package.json ADDED
@@ -0,0 +1,79 @@
1
+ {
2
+ "name": "@dashflow/ms365-mcp-server",
3
+ "version": "1.0.0",
4
+ "description": "A Model Context Protocol (MCP) server for interacting with Microsoft 365 and Office services through the Graph API",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "bin": {
8
+ "ms365-mcp-server": "dist/index.js"
9
+ },
10
+ "scripts": {
11
+ "generate": "node bin/generate-graph-client.mjs",
12
+ "build": "tsup",
13
+ "test": "vitest run",
14
+ "test:watch": "vitest",
15
+ "dev": "tsx src/index.ts",
16
+ "dev:http": "tsx --watch src/index.ts --http 127.0.0.1:3000 -v",
17
+ "format": "prettier --write \"**/*.{ts,mts,js,mjs,json,md}\"",
18
+ "format:check": "prettier --check \"**/*.{ts,mts,js,mjs,json,md}\"",
19
+ "lint": "eslint .",
20
+ "lint:fix": "eslint . --fix",
21
+ "verify": "npm run generate && npm run lint && npm run format:check && npm run build && npm run test",
22
+ "inspector": "npx @modelcontextprotocol/inspector tsx src/index.ts"
23
+ },
24
+ "keywords": [
25
+ "microsoft",
26
+ "365",
27
+ "mcp",
28
+ "server",
29
+ "dashflow"
30
+ ],
31
+ "author": "Dashflow",
32
+ "license": "MIT",
33
+ "publishConfig": {
34
+ "access": "public"
35
+ },
36
+ "dependencies": {
37
+ "@azure/msal-node": "^3.8.0",
38
+ "@modelcontextprotocol/sdk": "^1.25.0",
39
+ "@toon-format/toon": "^0.8.0",
40
+ "commander": "^11.1.0",
41
+ "dotenv": "^17.0.1",
42
+ "express": "^5.2.1",
43
+ "js-yaml": "^4.1.1",
44
+ "winston": "^3.17.0",
45
+ "zod": "^3.24.2"
46
+ },
47
+ "optionalDependencies": {
48
+ "@azure/identity": "^4.5.0",
49
+ "@azure/keyvault-secrets": "^4.9.0",
50
+ "keytar": "^7.9.0"
51
+ },
52
+ "devDependencies": {
53
+ "@redocly/cli": "^2.11.1",
54
+ "@semantic-release/exec": "^7.1.0",
55
+ "@semantic-release/git": "^10.0.1",
56
+ "@semantic-release/github": "^11.0.3",
57
+ "@semantic-release/npm": "^13.1.3",
58
+ "@types/express": "^5.0.3",
59
+ "@types/node": "^22.15.15",
60
+ "@typescript-eslint/eslint-plugin": "^8.38.0",
61
+ "@typescript-eslint/parser": "^8.38.0",
62
+ "@vitest/coverage-v8": "^3.2.4",
63
+ "eslint": "^9.31.0",
64
+ "globals": "^16.3.0",
65
+ "prettier": "^3.5.3",
66
+ "semantic-release": "^25.0.2",
67
+ "tsup": "^8.5.1",
68
+ "tsx": "^4.19.4",
69
+ "typescript": "^5.8.3",
70
+ "vitest": "^3.1.1"
71
+ },
72
+ "engines": {
73
+ "node": ">=18"
74
+ },
75
+ "repository": {
76
+ "type": "git",
77
+ "url": "https://github.com/dashflow/ms365-mcp-server.git"
78
+ }
79
+ }