@softeria/ms-365-mcp-server 0.116.0 → 0.117.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
@@ -575,8 +575,10 @@ Environment variables:
575
575
  - `MS365_MCP_MAX_TOP=<n>`: Hard cap for Graph `$top` / `top` on list requests (positive integer). When the model passes a larger value, the server clamps it to `n` so responses stay smaller. Example: `MS365_MCP_MAX_TOP=15`
576
576
  - `MS365_MCP_MAX_PAGES=<n>`: Maximum number of pages followed when a tool is called with `fetchAllPages: true` (positive integer, default `100`). Bounds memory and latency for large result sets.
577
577
  - `MS365_MCP_MAX_ITEMS=<n>`: Maximum number of items accumulated when `fetchAllPages: true` (positive integer, default `10000`). Pagination stops and the response is truncated once this many items are collected.
578
- - `MS365_MCP_ALLOW_PAGINATION=0|false|no`: Disable multi-page following entirely. When set, `fetchAllPages: true` returns only the first page (default: pagination enabled).
578
+ - `MS365_MCP_ALLOW_PAGINATION=0|false|no`: Disable multi-page following entirely. When set, the `fetchAllPages` parameter is not advertised on tools, and any request that still passes it returns only the first page (default: pagination enabled).
579
579
  - `MS365_MCP_BODY_FORMAT=html`: Return email bodies as HTML instead of plain text (default: text)
580
+ - `MS365_MCP_RATE_LIMIT_DISABLED=true|1`: Disable per-IP rate limiting in HTTP mode (default: enabled — 30 req/min on `/authorize`, `/token`, `/register`; 120 req/min on `/mcp`)
581
+ - `MS365_MCP_TRUST_PROXY_HOPS=<n>`: Number of trusted reverse-proxy hops in HTTP mode (default `1`). Accurate per-IP rate limiting depends on this matching your deployment — set to the number of proxies in front of the server, `0` to use the raw socket peer IP, or a comma-separated subnet list
580
582
  - `MS365_MCP_CLOUD_TYPE=global|china`: Microsoft cloud environment (alternative to --cloud flag)
581
583
  - `LOG_LEVEL`: Set logging level (default: 'info')
582
584
  - `SILENT=true|1`: Disable console output
@@ -252,6 +252,26 @@ describe("graph-tools", () => {
252
252
  registerGraphTools(server, graphClient);
253
253
  await server.tools.get("test-tool").handler({ fetchAllPages: true });
254
254
  expect(graphClient.graphRequest).toHaveBeenCalledTimes(1);
255
+ expect(server.tools.get("test-tool").schema.fetchAllPages).toBeUndefined();
256
+ });
257
+ it("should advertise fetchAllPages when pagination is enabled", async () => {
258
+ delete process.env.MS365_MCP_ALLOW_PAGINATION;
259
+ mockEndpoints.push(makeEndpoint());
260
+ mockEndpointsJson = [makeConfig()];
261
+ const server = createMockServer();
262
+ const { registerGraphTools } = await loadModule();
263
+ registerGraphTools(server, createMockGraphClient());
264
+ expect(server.tools.get("test-tool").schema.fetchAllPages).toBeDefined();
265
+ });
266
+ it("should reflect MS365_MCP_MAX_PAGES in the fetchAllPages description", async () => {
267
+ process.env.MS365_MCP_MAX_PAGES = "7";
268
+ mockEndpoints.push(makeEndpoint());
269
+ mockEndpointsJson = [makeConfig()];
270
+ const server = createMockServer();
271
+ const { registerGraphTools } = await loadModule();
272
+ registerGraphTools(server, createMockGraphClient());
273
+ const schema = server.tools.get("test-tool").schema.fetchAllPages;
274
+ expect(schema.description).toContain("up to 7 pages");
255
275
  });
256
276
  });
257
277
  });
@@ -542,9 +542,10 @@ function registerGraphTools(server, graphClient, readOnly = false, enabledToolsP
542
542
  paramSchema[pathParamName] = z.string().describe(`Path parameter: ${pathParamName}`);
543
543
  }
544
544
  }
545
- if (tool.method.toUpperCase() === "GET" && tool.path.includes("/")) {
545
+ if (tool.method.toUpperCase() === "GET" && tool.path.includes("/") && paginationAllowed()) {
546
+ const maxPages = positiveIntFromEnv("MS365_MCP_MAX_PAGES", DEFAULT_MAX_PAGES);
546
547
  paramSchema["fetchAllPages"] = z.boolean().describe(
547
- "Follow @odata.nextLink and merge up to 100 pages into one response. Can return enormous payloads\u2014only when the user explicitly needs a full export. Prefer a small $top first, then paginate or narrow with $filter/$search."
548
+ `Follow @odata.nextLink and merge up to ${maxPages} pages into one response. Can return enormous payloads\u2014only when the user explicitly needs a full export. Prefer a small $top first, then paginate or narrow with $filter/$search.`
548
549
  ).optional();
549
550
  }
550
551
  if (paramSchema["filter"] !== void 0 || paramSchema["$filter"] !== void 0) {
package/dist/server.js CHANGED
@@ -3,6 +3,8 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
3
3
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
4
4
  import { mcpAuthRouter } from "@modelcontextprotocol/sdk/server/auth/router.js";
5
5
  import express from "express";
6
+ import helmet from "helmet";
7
+ import rateLimit from "express-rate-limit";
6
8
  import logger, { enableConsoleLogging } from "./logger.js";
7
9
  import { registerAuthTools } from "./auth-tools.js";
8
10
  import { registerGraphTools, registerDiscoveryTools } from "./graph-tools.js";
@@ -167,7 +169,20 @@ class MicrosoftGraphServer {
167
169
  if (this.options.http) {
168
170
  const { host, port } = parseHttpOption(this.options.http);
169
171
  const app = express();
170
- app.set("trust proxy", true);
172
+ const trustProxyEnv = process.env.MS365_MCP_TRUST_PROXY_HOPS;
173
+ if (trustProxyEnv !== void 0 && trustProxyEnv !== "") {
174
+ const asNum = Number(trustProxyEnv);
175
+ app.set("trust proxy", Number.isFinite(asNum) ? asNum : trustProxyEnv);
176
+ } else {
177
+ app.set("trust proxy", 1);
178
+ }
179
+ app.use(
180
+ helmet({
181
+ contentSecurityPolicy: false,
182
+ crossOriginEmbedderPolicy: false,
183
+ hsts: { maxAge: 31536e3, includeSubDomains: true, preload: true }
184
+ })
185
+ );
171
186
  app.use(express.json());
172
187
  app.use(express.urlencoded({ extended: true }));
173
188
  const corsOrigin = process.env.MS365_MCP_CORS_ORIGIN || "http://localhost:3000";
@@ -184,6 +199,25 @@ class MicrosoftGraphServer {
184
199
  }
185
200
  next();
186
201
  });
202
+ const rateLimitDisabled = process.env.MS365_MCP_RATE_LIMIT_DISABLED === "true" || process.env.MS365_MCP_RATE_LIMIT_DISABLED === "1";
203
+ if (!rateLimitDisabled) {
204
+ const authLimiter = rateLimit({
205
+ windowMs: 6e4,
206
+ max: 30,
207
+ standardHeaders: "draft-7",
208
+ legacyHeaders: false
209
+ });
210
+ const mcpLimiter = rateLimit({
211
+ windowMs: 6e4,
212
+ max: 120,
213
+ standardHeaders: "draft-7",
214
+ legacyHeaders: false
215
+ });
216
+ app.use("/authorize", authLimiter);
217
+ app.use("/token", authLimiter);
218
+ app.use("/register", authLimiter);
219
+ app.use("/mcp", mcpLimiter);
220
+ }
187
221
  const oauthProvider = new MicrosoftOAuthProvider(this.authManager, this.secrets);
188
222
  const publicUrlRaw = this.options.publicUrl || process.env.MS365_MCP_PUBLIC_URL || this.options.baseUrl || process.env.MS365_MCP_BASE_URL || null;
189
223
  const publicBase = publicUrlRaw ? new URL(publicUrlRaw).href.replace(/\/$/, "") : null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@softeria/ms-365-mcp-server",
3
- "version": "0.116.0",
3
+ "version": "0.117.0",
4
4
  "description": " A Model Context Protocol (MCP) server for interacting with Microsoft 365 and Office services through the Graph API",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -39,6 +39,8 @@
39
39
  "commander": "^11.1.0",
40
40
  "dotenv": "^17.0.1",
41
41
  "express": "^5.2.1",
42
+ "express-rate-limit": "^7.5.1",
43
+ "helmet": "^8.1.0",
42
44
  "js-yaml": "^4.1.0",
43
45
  "open": "^11.0.0",
44
46
  "winston": "^3.17.0",