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

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;
@@ -204,7 +204,10 @@ class GraphClient {
204
204
  }
205
205
  async graphRequest(endpoint, options = {}) {
206
206
  try {
207
- logger.info(`Calling ${endpoint} with options: ${JSON.stringify(options)}`);
207
+ const { accessToken: _redacted, ...safeOptions } = options;
208
+ logger.info(
209
+ `Calling ${endpoint} with options: ${JSON.stringify(safeOptions)}${_redacted ? " [accessToken=REDACTED]" : ""}`
210
+ );
208
211
  const result = await this.makeRequest(endpoint, options);
209
212
  const outputFormat = options.forceJsonOutput ? "json" : this.outputFormat;
210
213
  return this.formatJsonResponse(
@@ -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,
@@ -1002,7 +1003,7 @@ function registerGraphTools(server, graphClient, readOnly = false, enabledToolsP
1002
1003
  for (const match of pathParamMatches) {
1003
1004
  const pathParamName = match[1];
1004
1005
  if (!(pathParamName in paramSchema)) {
1005
- paramSchema[pathParamName] = z.string().describe(`Path parameter: ${pathParamName}`);
1006
+ paramSchema[pathParamName] = z.string().describe(describePathParam(pathParamName));
1006
1007
  }
1007
1008
  }
1008
1009
  if (isFetchAllPagesApplicable(tool)) {
@@ -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
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@softeria/ms-365-mcp-server",
3
- "version": "0.134.1",
3
+ "version": "0.134.3",
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