@softeria/ms-365-mcp-server 0.133.2 → 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.
@@ -48,6 +48,7 @@ function makeEndpoint(overrides = {}) {
48
48
  { name: "search", type: "Query", schema: z.string().optional() },
49
49
  { name: "select", type: "Query", schema: z.string().optional() },
50
50
  { name: "orderby", type: "Query", schema: z.string().optional() },
51
+ { name: "expand", type: "Query", schema: z.string().optional() },
51
52
  { name: "count", type: "Query", schema: z.boolean().optional() },
52
53
  { name: "top", type: "Query", schema: z.number().optional() },
53
54
  { name: "skip", type: "Query", schema: z.number().optional() }
@@ -378,6 +379,20 @@ describe("graph-tools", () => {
378
379
  expect(schema["top"].description).toContain("Start small");
379
380
  expect(schema["top"].description).toContain("$select");
380
381
  });
382
+ it("should describe $expand as navigation-properties-only", async () => {
383
+ const endpoint = makeEndpoint();
384
+ const config = makeConfig();
385
+ mockEndpoints.push(endpoint);
386
+ mockEndpointsJson = [config];
387
+ const server = createMockServer();
388
+ const { registerGraphTools } = await loadModule();
389
+ registerGraphTools(server, createMockGraphClient());
390
+ const schema = server.tools.get("test-tool").schema;
391
+ expect(schema["expand"]).toBeDefined();
392
+ expect(schema["expand"].description).not.toBe("Expand related entities");
393
+ expect(schema["expand"].description).toContain("navigation");
394
+ expect(schema["expand"].description).toContain("attachments");
395
+ });
381
396
  });
382
397
  describe("MS365_MCP_MAX_TOP", () => {
383
398
  const prevMaxTop = process.env.MS365_MCP_MAX_TOP;
@@ -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,72 +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();
1023
+ }
1024
+ if (paramSchema["expand"] !== void 0 || paramSchema["$expand"] !== void 0) {
1025
+ const key = paramSchema["$expand"] !== void 0 ? "$expand" : "expand";
1026
+ paramSchema[key] = z.array(z.string()).describe(EXPAND_PARAM_DESCRIPTION).optional();
1025
1027
  }
1026
1028
  if (paramSchema["orderby"] !== void 0 || paramSchema["$orderby"] !== void 0) {
1027
1029
  const key = paramSchema["$orderby"] !== void 0 ? "$orderby" : "orderby";
1028
- paramSchema[key] = z.string().describe("Sort expression, e.g. receivedDateTime desc").optional();
1030
+ paramSchema[key] = z.string().describe(ORDERBY_PARAM_DESCRIPTION).optional();
1029
1031
  }
1030
- if (TOP_UNSUPPORTED_DELTA_TOOLS.has(tool.alias)) {
1032
+ if (shouldOmitTopParam(tool.alias)) {
1031
1033
  delete paramSchema["top"];
1032
1034
  delete paramSchema["$top"];
1033
1035
  } else if (paramSchema["top"] !== void 0 || paramSchema["$top"] !== void 0) {
1034
1036
  const key = paramSchema["$top"] !== void 0 ? "$top" : "top";
1035
- paramSchema[key] = z.number().describe(
1036
- "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."
1037
- ).optional();
1037
+ paramSchema[key] = z.number().describe(TOP_PARAM_DESCRIPTION).optional();
1038
1038
  }
1039
1039
  if (paramSchema["skip"] !== void 0 || paramSchema["$skip"] !== void 0) {
1040
1040
  const key = paramSchema["$skip"] !== void 0 ? "$skip" : "skip";
1041
- 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();
1042
1042
  }
1043
1043
  if (paramSchema["count"] !== void 0 || paramSchema["$count"] !== void 0) {
1044
1044
  const countKey = paramSchema["$count"] !== void 0 ? "$count" : "count";
1045
- paramSchema[countKey] = z.boolean().describe(
1046
- "Set true to enable advanced query mode (ConsistencyLevel: eventual). Required for complex $filter on flag/flagStatus or contains()."
1047
- ).optional();
1045
+ paramSchema[countKey] = z.boolean().describe(COUNT_PARAM_DESCRIPTION).optional();
1048
1046
  }
1049
1047
  if (multiAccount) {
1050
- const accountHint = accountNames.length > 0 ? `Known accounts: ${accountNames.join(", ")}. ` : "";
1051
- paramSchema["account"] = z.string().describe(
1052
- `${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.`
1053
- ).optional();
1048
+ paramSchema["account"] = z.string().describe(getAccountParamDescription(accountNames)).optional();
1054
1049
  }
1055
1050
  paramSchema["includeHeaders"] = z.boolean().describe("Include response headers (including ETag) in the response metadata").optional();
1056
1051
  paramSchema["excludeResponse"] = z.boolean().describe("Exclude the full response body and only return success or failure indication").optional();
1057
1052
  const destructive = isDestructiveOperation(tool.method, endpointConfig);
1058
1053
  if (destructive) {
1059
- paramSchema["confirm"] = z.boolean().describe(
1060
- '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.'
1061
- ).optional();
1054
+ paramSchema["confirm"] = z.boolean().describe(CONFIRM_PARAM_DESCRIPTION).optional();
1062
1055
  }
1063
1056
  if (endpointConfig?.supportsTimezone) {
1064
- paramSchema["timezone"] = z.string().describe(
1065
- 'IANA timezone name (e.g., "America/New_York", "Europe/London", "Asia/Tokyo") for calendar event times. If not specified, times are returned in UTC.'
1066
- ).optional();
1057
+ paramSchema["timezone"] = z.string().describe(TIMEZONE_PARAM_DESCRIPTION).optional();
1067
1058
  }
1068
1059
  if (endpointConfig?.supportsExpandExtendedProperties) {
1069
- paramSchema["expandExtendedProperties"] = z.boolean().describe(
1070
- "When true, expands singleValueExtendedProperties on each event. Use this to retrieve custom extended properties (e.g., sync metadata) stored on calendar events."
1071
- ).optional();
1060
+ paramSchema["expandExtendedProperties"] = z.boolean().describe(EXPAND_EXTENDED_PROPERTIES_PARAM_DESCRIPTION).optional();
1072
1061
  }
1073
1062
  let toolDescription = withApiVersionPrefix(
1074
1063
  (endpointConfig?.descriptionOverride ?? tool.description) || `Execute ${tool.method.toUpperCase()} request to ${tool.path}`,
@@ -1367,7 +1356,7 @@ function registerDiscoveryTools(server, graphClient, readOnly = false, orgMode =
1367
1356
  async ({ tool_name }) => {
1368
1357
  const entry = toolsRegistry.get(tool_name);
1369
1358
  if (entry) {
1370
- const schema = describeToolSchema(entry.tool, entry.config);
1359
+ const schema = describeToolSchema(entry.tool, entry.config, { multiAccount, accountNames });
1371
1360
  return {
1372
1361
  content: [{ type: "text", text: JSON.stringify(schema, null, 2) }]
1373
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.133.2",
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",