@softeria/ms-365-mcp-server 0.134.0 → 0.134.1

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.
@@ -20,15 +20,33 @@ import { getRequestTokens } from "./request-context.js";
20
20
  import { parseTeamsUrl } from "./lib/teams-url-parser.js";
21
21
  import { buildBM25Index, scoreQuery, tokenize } from "./lib/bm25.js";
22
22
  import { describeToolSchema, describeUtilityToolSchema } from "./lib/tool-schema.js";
23
+ import {
24
+ TOP_UNSUPPORTED_DELTA_TOOLS,
25
+ shouldOmitTopParam,
26
+ paginationAllowed,
27
+ positiveIntFromEnv,
28
+ DEFAULT_MAX_PAGES,
29
+ getMaxPages,
30
+ isFetchAllPagesApplicable,
31
+ FILTER_PARAM_DESCRIPTION,
32
+ SEARCH_PARAM_DESCRIPTION,
33
+ SELECT_PARAM_DESCRIPTION,
34
+ EXPAND_PARAM_DESCRIPTION,
35
+ ORDERBY_PARAM_DESCRIPTION,
36
+ TOP_PARAM_DESCRIPTION,
37
+ SKIP_PARAM_DESCRIPTION,
38
+ COUNT_PARAM_DESCRIPTION,
39
+ CONFIRM_PARAM_DESCRIPTION,
40
+ TIMEZONE_PARAM_DESCRIPTION,
41
+ EXPAND_EXTENDED_PROPERTIES_PARAM_DESCRIPTION,
42
+ getAccountParamDescription,
43
+ getFetchAllPagesParamDescription
44
+ } from "./lib/param-descriptions.js";
23
45
  const __filename = fileURLToPath(import.meta.url);
24
46
  const __dirname = path.dirname(__filename);
25
47
  const endpointsData = JSON.parse(
26
48
  readFileSync(path.join(__dirname, "endpoints.json"), "utf8")
27
49
  );
28
- const TOP_UNSUPPORTED_DELTA_TOOLS = /* @__PURE__ */ new Set([
29
- "list-calendar-events-delta",
30
- "list-calendar-view-delta"
31
- ]);
32
50
  function withApiVersionPrefix(description, config) {
33
51
  return config?.apiVersion === "beta" ? `[beta] ${description}` : description;
34
52
  }
@@ -52,23 +70,7 @@ function clampTopQueryParam(queryParams) {
52
70
  logger.info(`Clamping $top from ${requested} to ${cap} (MS365_MCP_MAX_TOP)`);
53
71
  queryParams["$top"] = String(cap);
54
72
  }
55
- const DEFAULT_MAX_PAGES = 100;
56
73
  const DEFAULT_MAX_ITEMS = 1e4;
57
- function positiveIntFromEnv(name, defaultValue) {
58
- const raw = process.env[name];
59
- if (raw === void 0 || raw === "") return defaultValue;
60
- const n = Number.parseInt(raw, 10);
61
- if (!Number.isFinite(n) || n < 1) {
62
- logger.warn(`Ignoring invalid ${name}=${JSON.stringify(raw)} (use a positive integer)`);
63
- return defaultValue;
64
- }
65
- return n;
66
- }
67
- function paginationAllowed() {
68
- const raw = process.env.MS365_MCP_ALLOW_PAGINATION;
69
- if (raw === void 0 || raw === "") return true;
70
- return !/^(0|false|no)$/i.test(raw.trim());
71
- }
72
74
  function isConfirmGateEnabled() {
73
75
  return process.env.MS365_MCP_REQUIRE_CONFIRM === "true";
74
76
  }
@@ -1003,78 +1005,59 @@ function registerGraphTools(server, graphClient, readOnly = false, enabledToolsP
1003
1005
  paramSchema[pathParamName] = z.string().describe(`Path parameter: ${pathParamName}`);
1004
1006
  }
1005
1007
  }
1006
- if (tool.method.toUpperCase() === "GET" && tool.path.includes("/") && paginationAllowed()) {
1007
- const maxPages = positiveIntFromEnv("MS365_MCP_MAX_PAGES", DEFAULT_MAX_PAGES);
1008
- paramSchema["fetchAllPages"] = z.boolean().describe(
1009
- `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.`
1010
- ).optional();
1008
+ if (isFetchAllPagesApplicable(tool)) {
1009
+ const maxPages = getMaxPages();
1010
+ paramSchema["fetchAllPages"] = z.boolean().describe(getFetchAllPagesParamDescription(maxPages)).optional();
1011
1011
  }
1012
1012
  if (paramSchema["filter"] !== void 0 || paramSchema["$filter"] !== void 0) {
1013
1013
  const key = paramSchema["$filter"] !== void 0 ? "$filter" : "filter";
1014
- paramSchema[key] = z.string().describe(
1015
- "OData filter expression. Add $count=true for advanced filters (flag/flagStatus, contains()). Cannot combine with $search."
1016
- ).optional();
1014
+ paramSchema[key] = z.string().describe(FILTER_PARAM_DESCRIPTION).optional();
1017
1015
  }
1018
1016
  if (paramSchema["search"] !== void 0 || paramSchema["$search"] !== void 0) {
1019
1017
  const key = paramSchema["$search"] !== void 0 ? "$search" : "search";
1020
- paramSchema[key] = z.string().describe("KQL search query \u2014 wrap value in double quotes. Cannot combine with $filter.").optional();
1018
+ paramSchema[key] = z.string().describe(SEARCH_PARAM_DESCRIPTION).optional();
1021
1019
  }
1022
1020
  if (paramSchema["select"] !== void 0 || paramSchema["$select"] !== void 0) {
1023
1021
  const key = paramSchema["$select"] !== void 0 ? "$select" : "select";
1024
- paramSchema[key] = z.string().describe("Comma-separated fields to return, e.g. id,subject,from,receivedDateTime").optional();
1022
+ paramSchema[key] = z.string().describe(SELECT_PARAM_DESCRIPTION).optional();
1025
1023
  }
1026
1024
  if (paramSchema["expand"] !== void 0 || paramSchema["$expand"] !== void 0) {
1027
1025
  const key = paramSchema["$expand"] !== void 0 ? "$expand" : "expand";
1028
- paramSchema[key] = z.array(z.string()).describe(
1029
- 'Navigation properties to inline, e.g. attachments on a message or event. Only navigation properties can be expanded: expanding a non-navigation property such as a message body fails with "Parsing OData Select and Expand failed", and an unsupported value may be ignored rather than reported. Request ordinary fields with $select instead.'
1030
- ).optional();
1026
+ paramSchema[key] = z.array(z.string()).describe(EXPAND_PARAM_DESCRIPTION).optional();
1031
1027
  }
1032
1028
  if (paramSchema["orderby"] !== void 0 || paramSchema["$orderby"] !== void 0) {
1033
1029
  const key = paramSchema["$orderby"] !== void 0 ? "$orderby" : "orderby";
1034
- paramSchema[key] = z.string().describe("Sort expression, e.g. receivedDateTime desc").optional();
1030
+ paramSchema[key] = z.string().describe(ORDERBY_PARAM_DESCRIPTION).optional();
1035
1031
  }
1036
- if (TOP_UNSUPPORTED_DELTA_TOOLS.has(tool.alias)) {
1032
+ if (shouldOmitTopParam(tool.alias)) {
1037
1033
  delete paramSchema["top"];
1038
1034
  delete paramSchema["$top"];
1039
1035
  } else if (paramSchema["top"] !== void 0 || paramSchema["$top"] !== void 0) {
1040
1036
  const key = paramSchema["$top"] !== void 0 ? "$top" : "top";
1041
- paramSchema[key] = z.number().describe(
1042
- "Page size (Graph $top). Start small (e.g. 5\u201315) so responses fit the model context; raise only if needed. Use $select to return fewer fields per item. For more rows, use @odata.nextLink from the response instead of a very large $top."
1043
- ).optional();
1037
+ paramSchema[key] = z.number().describe(TOP_PARAM_DESCRIPTION).optional();
1044
1038
  }
1045
1039
  if (paramSchema["skip"] !== void 0 || paramSchema["$skip"] !== void 0) {
1046
1040
  const key = paramSchema["$skip"] !== void 0 ? "$skip" : "skip";
1047
- paramSchema[key] = z.number().describe("Items to skip for pagination. Not supported with $search.").optional();
1041
+ paramSchema[key] = z.number().describe(SKIP_PARAM_DESCRIPTION).optional();
1048
1042
  }
1049
1043
  if (paramSchema["count"] !== void 0 || paramSchema["$count"] !== void 0) {
1050
1044
  const countKey = paramSchema["$count"] !== void 0 ? "$count" : "count";
1051
- paramSchema[countKey] = z.boolean().describe(
1052
- "Set true to enable advanced query mode (ConsistencyLevel: eventual). Required for complex $filter on flag/flagStatus or contains()."
1053
- ).optional();
1045
+ paramSchema[countKey] = z.boolean().describe(COUNT_PARAM_DESCRIPTION).optional();
1054
1046
  }
1055
1047
  if (multiAccount) {
1056
- const accountHint = accountNames.length > 0 ? `Known accounts: ${accountNames.join(", ")}. ` : "";
1057
- paramSchema["account"] = z.string().describe(
1058
- `${accountHint}Microsoft account email to use for this request. Required when multiple accounts are configured. Use the list-accounts tool to discover all currently available accounts.`
1059
- ).optional();
1048
+ paramSchema["account"] = z.string().describe(getAccountParamDescription(accountNames)).optional();
1060
1049
  }
1061
1050
  paramSchema["includeHeaders"] = z.boolean().describe("Include response headers (including ETag) in the response metadata").optional();
1062
1051
  paramSchema["excludeResponse"] = z.boolean().describe("Exclude the full response body and only return success or failure indication").optional();
1063
1052
  const destructive = isDestructiveOperation(tool.method, endpointConfig);
1064
1053
  if (destructive) {
1065
- paramSchema["confirm"] = z.boolean().describe(
1066
- 'For destructive operations when the confirm gate is enabled (MS365_MCP_REQUIRE_CONFIRM=true; off by default). Set to true only after the user has explicitly approved this action. When the gate is on, calls without confirm: true return { error: "confirmation_required" } without touching user data.'
1067
- ).optional();
1054
+ paramSchema["confirm"] = z.boolean().describe(CONFIRM_PARAM_DESCRIPTION).optional();
1068
1055
  }
1069
1056
  if (endpointConfig?.supportsTimezone) {
1070
- paramSchema["timezone"] = z.string().describe(
1071
- 'IANA timezone name (e.g., "America/New_York", "Europe/London", "Asia/Tokyo") for calendar event times. If not specified, times are returned in UTC.'
1072
- ).optional();
1057
+ paramSchema["timezone"] = z.string().describe(TIMEZONE_PARAM_DESCRIPTION).optional();
1073
1058
  }
1074
1059
  if (endpointConfig?.supportsExpandExtendedProperties) {
1075
- paramSchema["expandExtendedProperties"] = z.boolean().describe(
1076
- "When true, expands singleValueExtendedProperties on each event. Use this to retrieve custom extended properties (e.g., sync metadata) stored on calendar events."
1077
- ).optional();
1060
+ paramSchema["expandExtendedProperties"] = z.boolean().describe(EXPAND_EXTENDED_PROPERTIES_PARAM_DESCRIPTION).optional();
1078
1061
  }
1079
1062
  let toolDescription = withApiVersionPrefix(
1080
1063
  (endpointConfig?.descriptionOverride ?? tool.description) || `Execute ${tool.method.toUpperCase()} request to ${tool.path}`,
@@ -1373,7 +1356,7 @@ function registerDiscoveryTools(server, graphClient, readOnly = false, orgMode =
1373
1356
  async ({ tool_name }) => {
1374
1357
  const entry = toolsRegistry.get(tool_name);
1375
1358
  if (entry) {
1376
- const schema = describeToolSchema(entry.tool, entry.config);
1359
+ const schema = describeToolSchema(entry.tool, entry.config, { multiAccount, accountNames });
1377
1360
  return {
1378
1361
  content: [{ type: "text", text: JSON.stringify(schema, null, 2) }]
1379
1362
  };
@@ -0,0 +1,93 @@
1
+ import logger from "../logger.js";
2
+ const TOP_UNSUPPORTED_DELTA_TOOLS = /* @__PURE__ */ new Set([
3
+ "list-calendar-events-delta",
4
+ "list-calendar-view-delta"
5
+ ]);
6
+ function shouldOmitTopParam(toolAlias) {
7
+ return TOP_UNSUPPORTED_DELTA_TOOLS.has(toolAlias);
8
+ }
9
+ function paginationAllowed() {
10
+ const raw = process.env.MS365_MCP_ALLOW_PAGINATION;
11
+ if (raw === void 0 || raw === "") return true;
12
+ return !/^(0|false|no)$/i.test(raw.trim());
13
+ }
14
+ const DEFAULT_MAX_PAGES = 100;
15
+ function positiveIntFromEnv(name, defaultValue) {
16
+ const raw = process.env[name];
17
+ if (raw === void 0 || raw === "") return defaultValue;
18
+ const n = Number.parseInt(raw, 10);
19
+ if (!Number.isFinite(n) || n < 1) {
20
+ logger.warn(`Ignoring invalid ${name}=${JSON.stringify(raw)} (use a positive integer)`);
21
+ return defaultValue;
22
+ }
23
+ return n;
24
+ }
25
+ function getMaxPages() {
26
+ return positiveIntFromEnv("MS365_MCP_MAX_PAGES", DEFAULT_MAX_PAGES);
27
+ }
28
+ function isFetchAllPagesApplicable(tool) {
29
+ return tool.method.toUpperCase() === "GET" && tool.path.includes("/") && paginationAllowed();
30
+ }
31
+ const FILTER_PARAM_DESCRIPTION = "OData filter expression. Add $count=true for advanced filters (flag/flagStatus, contains()). Cannot combine with $search.";
32
+ const SEARCH_PARAM_DESCRIPTION = "KQL search query \u2014 wrap value in double quotes. Cannot combine with $filter.";
33
+ const SELECT_PARAM_DESCRIPTION = "Comma-separated fields to return, e.g. id,subject,from,receivedDateTime";
34
+ const EXPAND_PARAM_DESCRIPTION = 'Navigation properties to inline, e.g. attachments on a message or event. Only navigation properties can be expanded: expanding a non-navigation property such as a message body fails with "Parsing OData Select and Expand failed", and an unsupported value may be ignored rather than reported. Request ordinary fields with $select instead.';
35
+ const ORDERBY_PARAM_DESCRIPTION = "Sort expression, e.g. receivedDateTime desc";
36
+ const TOP_PARAM_DESCRIPTION = "Page size (Graph $top). Start small (e.g. 5\u201315) so responses fit the model context; raise only if needed. Use $select to return fewer fields per item. For more rows, use @odata.nextLink from the response instead of a very large $top.";
37
+ const SKIP_PARAM_DESCRIPTION = "Items to skip for pagination. Not supported with $search.";
38
+ const COUNT_PARAM_DESCRIPTION = "Set true to enable advanced query mode (ConsistencyLevel: eventual). Required for complex $filter on flag/flagStatus or contains().";
39
+ const CONFIRM_PARAM_DESCRIPTION = 'For destructive operations when the confirm gate is enabled (MS365_MCP_REQUIRE_CONFIRM=true; off by default). Set to true only after the user has explicitly approved this action. When the gate is on, calls without confirm: true return { error: "confirmation_required" } without touching user data.';
40
+ const TIMEZONE_PARAM_DESCRIPTION = 'IANA timezone name (e.g., "America/New_York", "Europe/London", "Asia/Tokyo") for calendar event times. If not specified, times are returned in UTC.';
41
+ const EXPAND_EXTENDED_PROPERTIES_PARAM_DESCRIPTION = "When true, expands singleValueExtendedProperties on each event. Use this to retrieve custom extended properties (e.g., sync metadata) stored on calendar events.";
42
+ function getAccountParamDescription(accountNames) {
43
+ const accountHint = accountNames.length > 0 ? `Known accounts: ${accountNames.join(", ")}. ` : "";
44
+ return `${accountHint}Microsoft account email to use for this request. Required when multiple accounts are configured. Use the list-accounts tool to discover all currently available accounts.`;
45
+ }
46
+ function getFetchAllPagesParamDescription(maxPages) {
47
+ return `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.`;
48
+ }
49
+ function getODataParamDescription(bareName) {
50
+ switch (bareName) {
51
+ case "filter":
52
+ return FILTER_PARAM_DESCRIPTION;
53
+ case "search":
54
+ return SEARCH_PARAM_DESCRIPTION;
55
+ case "select":
56
+ return SELECT_PARAM_DESCRIPTION;
57
+ case "expand":
58
+ return EXPAND_PARAM_DESCRIPTION;
59
+ case "orderby":
60
+ return ORDERBY_PARAM_DESCRIPTION;
61
+ case "top":
62
+ return TOP_PARAM_DESCRIPTION;
63
+ case "skip":
64
+ return SKIP_PARAM_DESCRIPTION;
65
+ case "count":
66
+ return COUNT_PARAM_DESCRIPTION;
67
+ default:
68
+ return void 0;
69
+ }
70
+ }
71
+ export {
72
+ CONFIRM_PARAM_DESCRIPTION,
73
+ COUNT_PARAM_DESCRIPTION,
74
+ DEFAULT_MAX_PAGES,
75
+ EXPAND_EXTENDED_PROPERTIES_PARAM_DESCRIPTION,
76
+ EXPAND_PARAM_DESCRIPTION,
77
+ FILTER_PARAM_DESCRIPTION,
78
+ ORDERBY_PARAM_DESCRIPTION,
79
+ SEARCH_PARAM_DESCRIPTION,
80
+ SELECT_PARAM_DESCRIPTION,
81
+ SKIP_PARAM_DESCRIPTION,
82
+ TIMEZONE_PARAM_DESCRIPTION,
83
+ TOP_PARAM_DESCRIPTION,
84
+ TOP_UNSUPPORTED_DELTA_TOOLS,
85
+ getAccountParamDescription,
86
+ getFetchAllPagesParamDescription,
87
+ getMaxPages,
88
+ getODataParamDescription,
89
+ isFetchAllPagesApplicable,
90
+ paginationAllowed,
91
+ positiveIntFromEnv,
92
+ shouldOmitTopParam
93
+ };
@@ -1,5 +1,16 @@
1
1
  import { zodToJsonSchema } from "zod-to-json-schema";
2
2
  import { isDestructiveOperation } from "./destructive-ops.js";
3
+ import {
4
+ getODataParamDescription,
5
+ shouldOmitTopParam,
6
+ isFetchAllPagesApplicable,
7
+ getMaxPages,
8
+ getFetchAllPagesParamDescription,
9
+ getAccountParamDescription,
10
+ CONFIRM_PARAM_DESCRIPTION,
11
+ TIMEZONE_PARAM_DESCRIPTION,
12
+ EXPAND_EXTENDED_PROPERTIES_PARAM_DESCRIPTION
13
+ } from "./param-descriptions.js";
3
14
  function unwrapOptional(schema) {
4
15
  const def = schema._def;
5
16
  const typeName = def?.typeName;
@@ -8,17 +19,22 @@ function unwrapOptional(schema) {
8
19
  }
9
20
  return { inner: schema, optional: false };
10
21
  }
11
- function describeToolSchema(tool, config) {
12
- const params = (tool.parameters ?? []).map((p) => {
22
+ function bareParamName(name) {
23
+ return name.startsWith("$") ? name.slice(1) : name;
24
+ }
25
+ function describeToolSchema(tool, config, ctx = {}) {
26
+ const omitTop = shouldOmitTopParam(tool.alias);
27
+ const params = (tool.parameters ?? []).filter((p) => !(omitTop && bareParamName(p.name) === "top")).map((p) => {
13
28
  const { inner, optional } = unwrapOptional(p.schema);
14
29
  const isPath = p.type === "Path";
15
30
  const jsonSchema = zodToJsonSchema(inner, { target: "jsonSchema7", $refStrategy: "none" });
16
31
  const { $schema: _s, ...schema } = jsonSchema;
32
+ const override = p.type === "Query" ? getODataParamDescription(bareParamName(p.name)) : void 0;
17
33
  return {
18
34
  name: p.name,
19
35
  in: p.type,
20
36
  required: isPath || !optional,
21
- description: p.description,
37
+ description: override ?? p.description,
22
38
  schema
23
39
  };
24
40
  });
@@ -27,7 +43,43 @@ function describeToolSchema(tool, config) {
27
43
  name: "confirm",
28
44
  in: "Query",
29
45
  required: false,
30
- description: 'For destructive operations when the confirm gate is enabled (MS365_MCP_REQUIRE_CONFIRM=true; off by default). Set to true only after the user has explicitly approved this action. When the gate is on, calls without confirm: true return { error: "confirmation_required" } without touching user data.',
46
+ description: CONFIRM_PARAM_DESCRIPTION,
47
+ schema: { type: "boolean" }
48
+ });
49
+ }
50
+ if (isFetchAllPagesApplicable({ method: tool.method, path: tool.path })) {
51
+ params.push({
52
+ name: "fetchAllPages",
53
+ in: "Query",
54
+ required: false,
55
+ description: getFetchAllPagesParamDescription(getMaxPages()),
56
+ schema: { type: "boolean" }
57
+ });
58
+ }
59
+ if (ctx.multiAccount) {
60
+ params.push({
61
+ name: "account",
62
+ in: "Query",
63
+ required: false,
64
+ description: getAccountParamDescription(ctx.accountNames ?? []),
65
+ schema: { type: "string" }
66
+ });
67
+ }
68
+ if (config?.supportsTimezone) {
69
+ params.push({
70
+ name: "timezone",
71
+ in: "Query",
72
+ required: false,
73
+ description: TIMEZONE_PARAM_DESCRIPTION,
74
+ schema: { type: "string" }
75
+ });
76
+ }
77
+ if (config?.supportsExpandExtendedProperties) {
78
+ params.push({
79
+ name: "expandExtendedProperties",
80
+ in: "Query",
81
+ required: false,
82
+ description: EXPAND_EXTENDED_PROPERTIES_PARAM_DESCRIPTION,
31
83
  schema: { type: "boolean" }
32
84
  });
33
85
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@softeria/ms-365-mcp-server",
3
- "version": "0.134.0",
3
+ "version": "0.134.1",
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",