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

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.
@@ -393,6 +393,19 @@ describe("graph-tools", () => {
393
393
  expect(schema["expand"].description).toContain("navigation");
394
394
  expect(schema["expand"].description).toContain("attachments");
395
395
  });
396
+ it("should describe path params it synthesizes itself", async () => {
397
+ const endpoint = makeEndpoint({ path: "/me/messages/:messageId" });
398
+ const config = makeConfig();
399
+ mockEndpoints.push(endpoint);
400
+ mockEndpointsJson = [config];
401
+ const server = createMockServer();
402
+ const { registerGraphTools } = await loadModule();
403
+ registerGraphTools(server, createMockGraphClient());
404
+ const schema = server.tools.get("test-tool").schema;
405
+ expect(schema["messageId"]).toBeDefined();
406
+ expect(schema["messageId"].description).not.toBe("Path parameter: messageId");
407
+ expect(schema["messageId"].description).toContain("not as 'id'");
408
+ });
396
409
  });
397
410
  describe("MS365_MCP_MAX_TOP", () => {
398
411
  const prevMaxTop = process.env.MS365_MCP_MAX_TOP;
@@ -1,7 +1,12 @@
1
1
  import { z } from "zod";
2
+ import { describePathParam } from "../lib/path-params.js";
2
3
  function makeApi(endpoints) {
3
4
  return endpoints;
4
5
  }
6
+ function isPathParamStub(param) {
7
+ const description = param.description ?? param.schema?.description;
8
+ return typeof description === "string" && /^Path parameter: /.test(description);
9
+ }
5
10
  class Zodios {
6
11
  constructor(baseUrlOrEndpoints, endpoints, options) {
7
12
  if (typeof baseUrlOrEndpoints === "string") {
@@ -19,17 +24,24 @@ class Zodios {
19
24
  pathParams.push(match[1]);
20
25
  }
21
26
  for (const pathParam of pathParams) {
22
- const paramExists = endpoint.parameters.some(
27
+ const existing = endpoint.parameters.find(
23
28
  (param) => param.name === pathParam || param.name === pathParam.replace(/[$_]+/g, "")
24
29
  );
25
- if (!paramExists) {
30
+ if (!existing) {
31
+ const description = describePathParam(pathParam);
26
32
  const newParam = {
27
33
  name: pathParam,
28
34
  type: "Path",
29
- schema: z.string().describe(`Path parameter: ${pathParam}`),
30
- description: `Path parameter: ${pathParam}`
35
+ schema: z.string().describe(description),
36
+ description
31
37
  };
32
38
  endpoint.parameters.push(newParam);
39
+ continue;
40
+ }
41
+ if (existing.type === "Path" && isPathParamStub(existing)) {
42
+ const description = describePathParam(existing.name);
43
+ existing.description = description;
44
+ existing.schema = z.string().describe(description);
33
45
  }
34
46
  }
35
47
  return endpoint;
@@ -2,6 +2,7 @@ import { randomUUID } from "crypto";
2
2
  import logger from "./logger.js";
3
3
  import { auditLog, getUserIdentityForAudit } from "./audit-log.js";
4
4
  import { isDestructiveOperation } from "./lib/destructive-ops.js";
5
+ import { describePathParam } from "./lib/path-params.js";
5
6
  import {
6
7
  getEndpointScopeGroups,
7
8
  getMissingAllowedScopesForGroups,
@@ -20,15 +21,33 @@ import { getRequestTokens } from "./request-context.js";
20
21
  import { parseTeamsUrl } from "./lib/teams-url-parser.js";
21
22
  import { buildBM25Index, scoreQuery, tokenize } from "./lib/bm25.js";
22
23
  import { describeToolSchema, describeUtilityToolSchema } from "./lib/tool-schema.js";
24
+ import {
25
+ TOP_UNSUPPORTED_DELTA_TOOLS,
26
+ shouldOmitTopParam,
27
+ paginationAllowed,
28
+ positiveIntFromEnv,
29
+ DEFAULT_MAX_PAGES,
30
+ getMaxPages,
31
+ isFetchAllPagesApplicable,
32
+ FILTER_PARAM_DESCRIPTION,
33
+ SEARCH_PARAM_DESCRIPTION,
34
+ SELECT_PARAM_DESCRIPTION,
35
+ EXPAND_PARAM_DESCRIPTION,
36
+ ORDERBY_PARAM_DESCRIPTION,
37
+ TOP_PARAM_DESCRIPTION,
38
+ SKIP_PARAM_DESCRIPTION,
39
+ COUNT_PARAM_DESCRIPTION,
40
+ CONFIRM_PARAM_DESCRIPTION,
41
+ TIMEZONE_PARAM_DESCRIPTION,
42
+ EXPAND_EXTENDED_PROPERTIES_PARAM_DESCRIPTION,
43
+ getAccountParamDescription,
44
+ getFetchAllPagesParamDescription
45
+ } from "./lib/param-descriptions.js";
23
46
  const __filename = fileURLToPath(import.meta.url);
24
47
  const __dirname = path.dirname(__filename);
25
48
  const endpointsData = JSON.parse(
26
49
  readFileSync(path.join(__dirname, "endpoints.json"), "utf8")
27
50
  );
28
- const TOP_UNSUPPORTED_DELTA_TOOLS = /* @__PURE__ */ new Set([
29
- "list-calendar-events-delta",
30
- "list-calendar-view-delta"
31
- ]);
32
51
  function withApiVersionPrefix(description, config) {
33
52
  return config?.apiVersion === "beta" ? `[beta] ${description}` : description;
34
53
  }
@@ -52,23 +71,7 @@ function clampTopQueryParam(queryParams) {
52
71
  logger.info(`Clamping $top from ${requested} to ${cap} (MS365_MCP_MAX_TOP)`);
53
72
  queryParams["$top"] = String(cap);
54
73
  }
55
- const DEFAULT_MAX_PAGES = 100;
56
74
  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
75
  function isConfirmGateEnabled() {
73
76
  return process.env.MS365_MCP_REQUIRE_CONFIRM === "true";
74
77
  }
@@ -1000,81 +1003,62 @@ function registerGraphTools(server, graphClient, readOnly = false, enabledToolsP
1000
1003
  for (const match of pathParamMatches) {
1001
1004
  const pathParamName = match[1];
1002
1005
  if (!(pathParamName in paramSchema)) {
1003
- paramSchema[pathParamName] = z.string().describe(`Path parameter: ${pathParamName}`);
1006
+ paramSchema[pathParamName] = z.string().describe(describePathParam(pathParamName));
1004
1007
  }
1005
1008
  }
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();
1009
+ if (isFetchAllPagesApplicable(tool)) {
1010
+ const maxPages = getMaxPages();
1011
+ paramSchema["fetchAllPages"] = z.boolean().describe(getFetchAllPagesParamDescription(maxPages)).optional();
1011
1012
  }
1012
1013
  if (paramSchema["filter"] !== void 0 || paramSchema["$filter"] !== void 0) {
1013
1014
  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();
1015
+ paramSchema[key] = z.string().describe(FILTER_PARAM_DESCRIPTION).optional();
1017
1016
  }
1018
1017
  if (paramSchema["search"] !== void 0 || paramSchema["$search"] !== void 0) {
1019
1018
  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();
1019
+ paramSchema[key] = z.string().describe(SEARCH_PARAM_DESCRIPTION).optional();
1021
1020
  }
1022
1021
  if (paramSchema["select"] !== void 0 || paramSchema["$select"] !== void 0) {
1023
1022
  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();
1023
+ paramSchema[key] = z.string().describe(SELECT_PARAM_DESCRIPTION).optional();
1025
1024
  }
1026
1025
  if (paramSchema["expand"] !== void 0 || paramSchema["$expand"] !== void 0) {
1027
1026
  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();
1027
+ paramSchema[key] = z.array(z.string()).describe(EXPAND_PARAM_DESCRIPTION).optional();
1031
1028
  }
1032
1029
  if (paramSchema["orderby"] !== void 0 || paramSchema["$orderby"] !== void 0) {
1033
1030
  const key = paramSchema["$orderby"] !== void 0 ? "$orderby" : "orderby";
1034
- paramSchema[key] = z.string().describe("Sort expression, e.g. receivedDateTime desc").optional();
1031
+ paramSchema[key] = z.string().describe(ORDERBY_PARAM_DESCRIPTION).optional();
1035
1032
  }
1036
- if (TOP_UNSUPPORTED_DELTA_TOOLS.has(tool.alias)) {
1033
+ if (shouldOmitTopParam(tool.alias)) {
1037
1034
  delete paramSchema["top"];
1038
1035
  delete paramSchema["$top"];
1039
1036
  } else if (paramSchema["top"] !== void 0 || paramSchema["$top"] !== void 0) {
1040
1037
  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();
1038
+ paramSchema[key] = z.number().describe(TOP_PARAM_DESCRIPTION).optional();
1044
1039
  }
1045
1040
  if (paramSchema["skip"] !== void 0 || paramSchema["$skip"] !== void 0) {
1046
1041
  const key = paramSchema["$skip"] !== void 0 ? "$skip" : "skip";
1047
- paramSchema[key] = z.number().describe("Items to skip for pagination. Not supported with $search.").optional();
1042
+ paramSchema[key] = z.number().describe(SKIP_PARAM_DESCRIPTION).optional();
1048
1043
  }
1049
1044
  if (paramSchema["count"] !== void 0 || paramSchema["$count"] !== void 0) {
1050
1045
  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();
1046
+ paramSchema[countKey] = z.boolean().describe(COUNT_PARAM_DESCRIPTION).optional();
1054
1047
  }
1055
1048
  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();
1049
+ paramSchema["account"] = z.string().describe(getAccountParamDescription(accountNames)).optional();
1060
1050
  }
1061
1051
  paramSchema["includeHeaders"] = z.boolean().describe("Include response headers (including ETag) in the response metadata").optional();
1062
1052
  paramSchema["excludeResponse"] = z.boolean().describe("Exclude the full response body and only return success or failure indication").optional();
1063
1053
  const destructive = isDestructiveOperation(tool.method, endpointConfig);
1064
1054
  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();
1055
+ paramSchema["confirm"] = z.boolean().describe(CONFIRM_PARAM_DESCRIPTION).optional();
1068
1056
  }
1069
1057
  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();
1058
+ paramSchema["timezone"] = z.string().describe(TIMEZONE_PARAM_DESCRIPTION).optional();
1073
1059
  }
1074
1060
  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();
1061
+ paramSchema["expandExtendedProperties"] = z.boolean().describe(EXPAND_EXTENDED_PROPERTIES_PARAM_DESCRIPTION).optional();
1078
1062
  }
1079
1063
  let toolDescription = withApiVersionPrefix(
1080
1064
  (endpointConfig?.descriptionOverride ?? tool.description) || `Execute ${tool.method.toUpperCase()} request to ${tool.path}`,
@@ -1373,7 +1357,7 @@ function registerDiscoveryTools(server, graphClient, readOnly = false, orgMode =
1373
1357
  async ({ tool_name }) => {
1374
1358
  const entry = toolsRegistry.get(tool_name);
1375
1359
  if (entry) {
1376
- const schema = describeToolSchema(entry.tool, entry.config);
1360
+ const schema = describeToolSchema(entry.tool, entry.config, { multiAccount, accountNames });
1377
1361
  return {
1378
1362
  content: [{ type: "text", text: JSON.stringify(schema, null, 2) }]
1379
1363
  };
@@ -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
+ };
@@ -0,0 +1,16 @@
1
+ function describePathParam(pathParamName) {
2
+ const match = /^(.*?)Id(\d*)$/.exec(pathParamName);
3
+ if (match && match[1]) {
4
+ const base = `Value for the '${pathParamName}' path segment. Pass it under the name '${pathParamName}', not as 'id'. `;
5
+ if (match[2]) {
6
+ return base + `It identifies a different resource from the similarly named parameter without the number; check the tool's path to see which.`;
7
+ }
8
+ const resource = match[1].replace(/([a-z0-9])([A-Z])/g, "$1 $2").toLowerCase();
9
+ const noun = /(^|\s)object$/.test(resource) ? resource : `${resource} object`;
10
+ return base + `Use the 'id' field of the ${noun} as returned by Microsoft Graph.`;
11
+ }
12
+ return `Value for the '${pathParamName}' path segment.`;
13
+ }
14
+ export {
15
+ describePathParam
16
+ };
@@ -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.2",
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",
@@ -1,10 +1,17 @@
1
1
  import { Endpoint, Parameter } from './endpoint-types.js';
2
2
  import { z } from 'zod';
3
+ import { describePathParam } from '../lib/path-params.js';
3
4
 
4
5
  export function makeApi(endpoints: Endpoint[]) {
5
6
  return endpoints;
6
7
  }
7
8
 
9
+ /** True when a path parameter carries only the generated `Path parameter: x` placeholder. */
10
+ function isPathParamStub(param: Parameter): boolean {
11
+ const description = param.description ?? param.schema?.description;
12
+ return typeof description === 'string' && /^Path parameter: /.test(description);
13
+ }
14
+
8
15
  export class Zodios {
9
16
  endpoints: Endpoint[];
10
17
 
@@ -26,18 +33,30 @@ export class Zodios {
26
33
  }
27
34
 
28
35
  for (const pathParam of pathParams) {
29
- const paramExists = endpoint.parameters.some(
36
+ const existing = endpoint.parameters.find(
30
37
  (param) => param.name === pathParam || param.name === pathParam.replace(/[$_]+/g, '')
31
38
  );
32
39
 
33
- if (!paramExists) {
40
+ if (!existing) {
41
+ const description = describePathParam(pathParam);
34
42
  const newParam: Parameter = {
35
43
  name: pathParam,
36
44
  type: 'Path',
37
- schema: z.string().describe(`Path parameter: ${pathParam}`),
38
- description: `Path parameter: ${pathParam}`,
45
+ schema: z.string().describe(description),
46
+ description,
39
47
  };
40
48
  endpoint.parameters.push(newParam);
49
+ continue;
50
+ }
51
+
52
+ // Some operations arrive with the path parameter already declared, but described
53
+ // only as `Path parameter: drive-id` — no more use to the model than nothing, and
54
+ // spelled in kebab-case so it does not even match the name callers must send.
55
+ // Upgrade those; any real upstream description is left alone.
56
+ if (existing.type === 'Path' && isPathParamStub(existing)) {
57
+ const description = describePathParam(existing.name);
58
+ existing.description = description;
59
+ existing.schema = z.string().describe(description);
41
60
  }
42
61
  }
43
62