@softeria/ms-365-mcp-server 0.132.0 → 0.133.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
@@ -27,6 +27,7 @@ This server supports multiple Microsoft cloud environments:
27
27
  - Comprehensive Microsoft 365 service integration
28
28
  - Read-only mode support for safe operations
29
29
  - Tool filtering for granular access control
30
+ - [Tool presets](#tool-presets) and [dynamic discovery](#dynamic-tool-discovery) to shrink the tool surface and token usage
30
31
 
31
32
  ## Output Format: JSON vs TOON
32
33
 
@@ -94,7 +95,7 @@ MS365_MCP_OUTPUT_FORMAT=toon npx @softeria/ms-365-mcp-server
94
95
 
95
96
  ## Supported Services & Tools
96
97
 
97
- The server provides 200+ tools covering most of the Microsoft Graph API surface. Each tool maps 1-to-1 to a Graph API endpoint and is defined declaratively in [`src/endpoints.json`](src/endpoints.json).
98
+ The server provides 300+ tools covering most of the Microsoft Graph API surface. Each tool maps 1-to-1 to a Graph API endpoint and is defined declaratively in [`src/endpoints.json`](src/endpoints.json).
98
99
 
99
100
  ### Personal Account Tools (Available by default)
100
101
 
@@ -509,7 +510,7 @@ Pinning is opt-in and local-MSAL only:
509
510
 
510
511
  ## Tool Presets
511
512
 
512
- To reduce initial connection overhead, use preset tool categories instead of loading all 90+ tools:
513
+ To reduce initial connection overhead and token usage, use preset tool categories instead of loading the full tool set:
513
514
 
514
515
  ```bash
515
516
  npx @softeria/ms-365-mcp-server --preset mail
@@ -518,7 +519,7 @@ npx @softeria/ms-365-mcp-server --list-presets # See all available presets
518
519
 
519
520
  Available presets: `mail`, `calendar`, `files`, `personal`, `work`, `excel`, `contacts`, `tasks`, `onenote`, `search`, `users`, `outlook`, `onedrive`, `teams`, `all`
520
521
 
521
- Each endpoint in `endpoints.json` declares which presets it belongs to via a `presets` array, so every preset is an exact tool-name allow-list that never over-matches across apps (e.g. `mail` does not include shared-mailbox tools; those are in `work`).
522
+ Each endpoint in `endpoints.json` declares which presets it belongs to via a `presets` array, so every preset is an exact tool-name allow-list that never over-matches across apps (e.g. `mail` does not include shared-mailbox tools; those are in `work`). The universal binary reader `download-bytes` is included in every preset, so whatever an app returns (a file, an attachment, a photo, a recording) can always be fetched; `get-download-url` (a pre-authenticated URL for drive/SharePoint files) rides with the drive-backed presets. So a preset that can find a file can always read its bytes.
522
523
 
523
524
  The `outlook`, `onedrive` and `teams` presets are app-scoped: they expose exactly one Microsoft app. Use these for "expose exactly one app" deployments:
524
525
 
@@ -532,7 +533,7 @@ npx @softeria/ms-365-mcp-server --org-mode --preset teams
532
533
 
533
534
  ## Dynamic Tool Discovery
534
535
 
535
- Instead of loading all 90+ tools upfront, use dynamic discovery so the LLM finds and loads tools only when it needs them:
536
+ Instead of loading every tool upfront, use dynamic discovery so the LLM finds and loads tools only when it needs them:
536
537
 
537
538
  ```bash
538
539
  npx @softeria/ms-365-mcp-server --discovery
@@ -1,5 +1,8 @@
1
1
  import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
2
  import { z } from "zod";
3
+ import { tmpdir } from "os";
4
+ import { join } from "path";
5
+ import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "fs";
3
6
  vi.mock("../logger.js", () => ({
4
7
  default: {
5
8
  info: vi.fn(),
@@ -744,6 +747,145 @@ describe("graph-tools", () => {
744
747
  expect(payload.error).toMatch(/relative Microsoft Graph path/);
745
748
  });
746
749
  });
750
+ describe("download-bytes-to-file", () => {
751
+ let tmpDir;
752
+ beforeEach(() => {
753
+ tmpDir = mkdtempSync(join(tmpdir(), "dbtf-"));
754
+ });
755
+ afterEach(() => {
756
+ rmSync(tmpDir, { recursive: true, force: true });
757
+ });
758
+ it("writes decoded binary bytes to the output path and returns metadata", async () => {
759
+ mockEndpoints.length = 0;
760
+ mockEndpointsJson = [];
761
+ const graphClient = {
762
+ makeRequest: vi.fn().mockResolvedValue({
763
+ message: "OK!",
764
+ contentType: "image/jpeg",
765
+ encoding: "base64",
766
+ contentLength: 2,
767
+ contentBytes: "aGk="
768
+ })
769
+ };
770
+ const server = createMockServer();
771
+ const { registerGraphTools } = await loadModule();
772
+ registerGraphTools(server, graphClient);
773
+ const tool = server.tools.get("download-bytes-to-file");
774
+ expect(tool).toBeDefined();
775
+ const outputPath = join(tmpDir, "photo.jpg");
776
+ const result = await tool.handler({ target: "/me/photo/$value", outputPath });
777
+ expect(graphClient.makeRequest).toHaveBeenCalledTimes(1);
778
+ const [reqPath, options] = graphClient.makeRequest.mock.calls[0];
779
+ expect(reqPath).toBe("/me/photo/$value");
780
+ expect(options.rawResponse).toBe(true);
781
+ expect(result.isError).toBeUndefined();
782
+ const payload = JSON.parse(result.content[0].text);
783
+ expect(payload).toEqual({ path: outputPath, contentType: "image/jpeg", bytesWritten: 2 });
784
+ expect(existsSync(outputPath)).toBe(true);
785
+ expect(readFileSync(outputPath).toString("utf8")).toBe("hi");
786
+ });
787
+ it("writes a text/JSON body verbatim from rawResponse", async () => {
788
+ mockEndpoints.length = 0;
789
+ mockEndpointsJson = [];
790
+ const graphClient = {
791
+ makeRequest: vi.fn().mockResolvedValue({
792
+ message: "OK!",
793
+ contentType: "text/plain",
794
+ rawResponse: "line1\nline2\n"
795
+ })
796
+ };
797
+ const server = createMockServer();
798
+ const { registerGraphTools } = await loadModule();
799
+ registerGraphTools(server, graphClient);
800
+ const outputPath = join(tmpDir, "note.txt");
801
+ const result = await server.tools.get("download-bytes-to-file").handler({ target: "/me/drive/root:/note.txt:/content", outputPath });
802
+ expect(result.isError).toBeUndefined();
803
+ expect(readFileSync(outputPath).toString("utf8")).toBe("line1\nline2\n");
804
+ });
805
+ it("rejects a relative outputPath", async () => {
806
+ mockEndpoints.length = 0;
807
+ mockEndpointsJson = [];
808
+ const graphClient = { makeRequest: vi.fn() };
809
+ const server = createMockServer();
810
+ const { registerGraphTools } = await loadModule();
811
+ registerGraphTools(server, graphClient);
812
+ const result = await server.tools.get("download-bytes-to-file").handler({ target: "/me/photo/$value", outputPath: "relative/path.jpg" });
813
+ expect(result.isError).toBe(true);
814
+ const payload = JSON.parse(result.content[0].text);
815
+ expect(payload.error).toMatch(/absolute path/);
816
+ expect(graphClient.makeRequest).not.toHaveBeenCalled();
817
+ });
818
+ it("rejects absolute URLs in target (Graph paths only)", async () => {
819
+ mockEndpoints.length = 0;
820
+ mockEndpointsJson = [];
821
+ const server = createMockServer();
822
+ const { registerGraphTools } = await loadModule();
823
+ registerGraphTools(server, {});
824
+ const result = await server.tools.get("download-bytes-to-file").handler({
825
+ target: "https://example.sharepoint.com/d/abc?temp=signed",
826
+ outputPath: join(tmpDir, "x.bin")
827
+ });
828
+ expect(result.isError).toBe(true);
829
+ const payload = JSON.parse(result.content[0].text);
830
+ expect(payload.error).toMatch(/relative Microsoft Graph path/);
831
+ });
832
+ it("refuses to overwrite an existing file and skips the Graph call", async () => {
833
+ mockEndpoints.length = 0;
834
+ mockEndpointsJson = [];
835
+ const outputPath = join(tmpDir, "existing.bin");
836
+ writeFileSync(outputPath, "original");
837
+ const graphClient = { makeRequest: vi.fn() };
838
+ const server = createMockServer();
839
+ const { registerGraphTools } = await loadModule();
840
+ registerGraphTools(server, graphClient);
841
+ const result = await server.tools.get("download-bytes-to-file").handler({ target: "/me/photo/$value", outputPath });
842
+ expect(result.isError).toBe(true);
843
+ const payload = JSON.parse(result.content[0].text);
844
+ expect(payload.error).toMatch(/already exists/);
845
+ expect(graphClient.makeRequest).not.toHaveBeenCalled();
846
+ expect(readFileSync(outputPath).toString("utf8")).toBe("original");
847
+ });
848
+ it("surfaces a Graph error and writes no file when makeRequest throws", async () => {
849
+ mockEndpoints.length = 0;
850
+ mockEndpointsJson = [];
851
+ const graphClient = {
852
+ makeRequest: vi.fn().mockRejectedValue(new Error("Microsoft Graph API error: 404 Not Found"))
853
+ };
854
+ const server = createMockServer();
855
+ const { registerGraphTools } = await loadModule();
856
+ registerGraphTools(server, graphClient);
857
+ const outputPath = join(tmpDir, "missing.bin");
858
+ const result = await server.tools.get("download-bytes-to-file").handler({ target: "/me/messages/abc/attachments/xyz/$value", outputPath });
859
+ expect(result.isError).toBe(true);
860
+ const payload = JSON.parse(result.content[0].text);
861
+ expect(payload.error).toMatch(/404 Not Found/);
862
+ expect(existsSync(outputPath)).toBe(false);
863
+ });
864
+ it("is registered in stdio mode but hidden in HTTP mode", async () => {
865
+ mockEndpoints.length = 0;
866
+ mockEndpointsJson = [];
867
+ const { registerGraphTools } = await loadModule();
868
+ const stdioServer = createMockServer();
869
+ registerGraphTools(stdioServer, {});
870
+ expect(stdioServer.tools.has("download-bytes-to-file")).toBe(true);
871
+ expect(stdioServer.tools.has("download-bytes")).toBe(true);
872
+ const httpServer = createMockServer();
873
+ registerGraphTools(
874
+ httpServer,
875
+ {},
876
+ false,
877
+ void 0,
878
+ false,
879
+ void 0,
880
+ false,
881
+ [],
882
+ void 0,
883
+ true
884
+ );
885
+ expect(httpServer.tools.has("download-bytes-to-file")).toBe(false);
886
+ expect(httpServer.tools.has("download-bytes")).toBe(true);
887
+ });
888
+ });
747
889
  describe("get-download-url", () => {
748
890
  it("strips /content, fetches item metadata, and returns the pre-authed downloadUrl", async () => {
749
891
  mockEndpoints.length = 0;
@@ -12,6 +12,7 @@ import { api as betaApi } from "./generated/client-beta.js";
12
12
  const allEndpoints = [...api.endpoints, ...betaApi.endpoints];
13
13
  import { z } from "zod";
14
14
  import { readFileSync } from "fs";
15
+ import { writeFile, access } from "fs/promises";
15
16
  import path from "path";
16
17
  import { fileURLToPath } from "url";
17
18
  import { TOOL_CATEGORIES } from "./tool-categories.js";
@@ -186,6 +187,161 @@ const UTILITY_TOOLS = [
186
187
  }
187
188
  }
188
189
  },
190
+ {
191
+ name: "download-bytes-to-file",
192
+ method: "GET",
193
+ path: "tool:download-bytes-to-file",
194
+ searchKeywords: "save to disk save to file write file to disk save attachment to disk save recording to disk write bytes to local file output path",
195
+ // Front-loaded on purpose: the discovery search index caps a tool's
196
+ // description at ~40 tokens, so the OneDrive/SharePoint guidance below
197
+ // sits past the cap. That keeps the hint for the reading LLM while letting
198
+ // get-download-url own the high-signal "drive"/"sharepoint" search terms.
199
+ description: "Write authenticated Microsoft Graph byte content to a local file on the server, returning { path, contentType, bytesWritten } instead of base64. The only out-of-band way to save mail attachments and meeting recordings, whose bytes are exposed solely through authenticated endpoints. Also handles profile photos and Teams hosted content. Writes to an absolute outputPath and never overwrites an existing file. stdio mode only: not available over HTTP. For OneDrive or SharePoint file content, get-download-url is preferred \u2014 it returns a pre-authenticated URL for fully out-of-band download without the server fetching the bytes.",
200
+ readOnlyHint: true,
201
+ openWorldHint: true,
202
+ stdioOnly: true,
203
+ buildSchema: (ctx) => {
204
+ const schema = {
205
+ target: z.string().describe(
206
+ 'Relative Microsoft Graph path starting with "/". Common paths: /drives/{drive-id}/items/{driveItem-id}/content (drive file content); /me/messages/{message-id}/attachments/{attachment-id}/$value (mail attachment, list-mail-attachments returns the IDs); /me/photo/$value or /users/{user-id}/photo/$value (profile photo); /chats/{chat-id}/messages/{chatMessage-id}/hostedContents/{chatMessageHostedContent-id}/$value (Teams chat hosted content, list-chat-message-hosted-contents returns the IDs); /teams/{team-id}/channels/{channel-id}/messages/{chatMessage-id}/hostedContents/{chatMessageHostedContent-id}/$value (Teams channel hosted content). For meeting recordings, use get-meeting-recording-content where available; Microsoft Graph returns authenticated recording bytes, not a pre-authenticated download URL.'
207
+ ),
208
+ outputPath: z.string().describe(
209
+ "Absolute path on the server's filesystem where the bytes are written, e.g. /Users/me/downloads/invoice.pdf. Must be absolute; relative paths are rejected. The parent directory must already exist, and an existing file is never overwritten (the call errors if outputPath already exists)."
210
+ )
211
+ };
212
+ if (ctx.multiAccount) {
213
+ schema["account"] = z.string().optional().describe(
214
+ "Account to use when multiple Microsoft accounts are configured. Required when multiple accounts exist (see list-accounts)."
215
+ );
216
+ }
217
+ return schema;
218
+ },
219
+ execute: async (params, { graphClient, authManager }) => {
220
+ const target = params.target;
221
+ const outputPath = params.outputPath;
222
+ const accountParam = params.account;
223
+ if (typeof target !== "string" || target.length === 0) {
224
+ return {
225
+ content: [
226
+ {
227
+ type: "text",
228
+ text: JSON.stringify({ error: "target is required and must be a non-empty string." })
229
+ }
230
+ ],
231
+ isError: true
232
+ };
233
+ }
234
+ if (!target.startsWith("/")) {
235
+ return {
236
+ content: [
237
+ {
238
+ type: "text",
239
+ text: JSON.stringify({
240
+ error: 'target must be a relative Microsoft Graph path starting with "/", e.g. /me/photo/$value or /drives/{drive-id}/items/{driveItem-id}/content. Absolute URLs are not accepted; if you have an @microsoft.graph.downloadUrl, use the equivalent /content or /$value path instead (Graph 302-redirects to the same bytes).'
241
+ })
242
+ }
243
+ ],
244
+ isError: true
245
+ };
246
+ }
247
+ if (typeof outputPath !== "string" || outputPath.length === 0) {
248
+ return {
249
+ content: [
250
+ {
251
+ type: "text",
252
+ text: JSON.stringify({
253
+ error: "outputPath is required and must be a non-empty string."
254
+ })
255
+ }
256
+ ],
257
+ isError: true
258
+ };
259
+ }
260
+ if (!path.isAbsolute(outputPath)) {
261
+ return {
262
+ content: [
263
+ {
264
+ type: "text",
265
+ text: JSON.stringify({
266
+ error: `outputPath must be an absolute path, e.g. /Users/me/downloads/file.ext. Received: ${outputPath}`
267
+ })
268
+ }
269
+ ],
270
+ isError: true
271
+ };
272
+ }
273
+ let fileExists = false;
274
+ try {
275
+ await access(outputPath);
276
+ fileExists = true;
277
+ } catch {
278
+ }
279
+ if (fileExists) {
280
+ return {
281
+ content: [
282
+ {
283
+ type: "text",
284
+ text: JSON.stringify({ error: `file already exists at ${outputPath}` })
285
+ }
286
+ ],
287
+ isError: true
288
+ };
289
+ }
290
+ try {
291
+ const accountModeError = await checkAccountParamInBearerMode(accountParam, authManager);
292
+ if (accountModeError) {
293
+ return {
294
+ content: [{ type: "text", text: JSON.stringify({ error: accountModeError }) }],
295
+ isError: true
296
+ };
297
+ }
298
+ let accountAccessToken;
299
+ if (authManager && !authManager.isOAuthModeEnabled() && !getRequestTokens()) {
300
+ accountAccessToken = await authManager.getTokenForAccount(accountParam);
301
+ }
302
+ const result = await graphClient.makeRequest(target, {
303
+ accessToken: accountAccessToken,
304
+ rawResponse: true
305
+ });
306
+ let buffer;
307
+ if (result.encoding === "base64" && typeof result.contentBytes === "string") {
308
+ buffer = Buffer.from(result.contentBytes, "base64");
309
+ } else if (typeof result.rawResponse === "string") {
310
+ buffer = Buffer.from(result.rawResponse, "utf8");
311
+ } else {
312
+ return {
313
+ content: [
314
+ {
315
+ type: "text",
316
+ text: JSON.stringify({
317
+ error: "Graph response contained no downloadable bytes for this target."
318
+ })
319
+ }
320
+ ],
321
+ isError: true
322
+ };
323
+ }
324
+ await writeFile(outputPath, buffer, { flag: "wx" });
325
+ return {
326
+ content: [
327
+ {
328
+ type: "text",
329
+ text: JSON.stringify({
330
+ path: outputPath,
331
+ contentType: result.contentType,
332
+ bytesWritten: buffer.byteLength
333
+ })
334
+ }
335
+ ]
336
+ };
337
+ } catch (error) {
338
+ return {
339
+ content: [{ type: "text", text: JSON.stringify({ error: error.message }) }],
340
+ isError: true
341
+ };
342
+ }
343
+ }
344
+ },
189
345
  {
190
346
  name: "get-download-url",
191
347
  method: "GET",
@@ -810,7 +966,7 @@ async function executeGraphTool(tool, config, graphClient, params, authManager)
810
966
  };
811
967
  }
812
968
  }
813
- function registerGraphTools(server, graphClient, readOnly = false, enabledToolsPattern, orgMode = false, authManager, multiAccount = false, accountNames = [], allowedScopesValue) {
969
+ function registerGraphTools(server, graphClient, readOnly = false, enabledToolsPattern, orgMode = false, authManager, multiAccount = false, accountNames = [], allowedScopesValue, httpMode = false) {
814
970
  let enabledToolsRegex;
815
971
  if (enabledToolsPattern) {
816
972
  try {
@@ -983,6 +1139,7 @@ function registerGraphTools(server, graphClient, readOnly = false, enabledToolsP
983
1139
  };
984
1140
  for (const utility of UTILITY_TOOLS) {
985
1141
  if (readOnly && !utility.readOnlyHint) continue;
1142
+ if (httpMode && utility.stdioOnly) continue;
986
1143
  if (enabledToolsRegex && !enabledToolsRegex.test(utility.name)) continue;
987
1144
  try {
988
1145
  registerUtilityToolWithMcp(server, utility, utilityCtx);
@@ -1098,7 +1255,7 @@ function scoreDiscoveryQuery(query, index) {
1098
1255
  ranked.sort((a, b) => b.score - a.score);
1099
1256
  return ranked;
1100
1257
  }
1101
- function registerDiscoveryTools(server, graphClient, readOnly = false, orgMode = false, authManager, multiAccount = false, accountNames = [], enabledTools, allowedScopesValue) {
1258
+ function registerDiscoveryTools(server, graphClient, readOnly = false, orgMode = false, authManager, multiAccount = false, accountNames = [], enabledTools, allowedScopesValue, httpMode = false) {
1102
1259
  let enabledToolsRegex;
1103
1260
  if (enabledTools) {
1104
1261
  try {
@@ -1125,6 +1282,7 @@ function registerDiscoveryTools(server, graphClient, readOnly = false, orgMode =
1125
1282
  }
1126
1283
  const utilityTools = UTILITY_TOOLS.filter((u) => {
1127
1284
  if (readOnly && !u.readOnlyHint) return false;
1285
+ if (httpMode && u.stdioOnly) return false;
1128
1286
  if (enabledToolsRegex && !enabledToolsRegex.test(u.name)) return false;
1129
1287
  return true;
1130
1288
  });
@@ -6,7 +6,7 @@ function buildGeneralMcpInstructions(opts) {
6
6
  "When you need an organizational user or recipient address, resolve it with list-users (or another directory tool); do not invent SMTP addresses.",
7
7
  "Directory $search on collections such as /users or /groups requires ConsistencyLevel: eventual when the tool exposes that header.",
8
8
  "Teams chat and channel messages: prefer HTML contentType in the body; plain text is often mangled by Graph.",
9
- "Files / binary content: for large drive/SharePoint file content, prefer get-download-url to resolve a pre-authenticated URL for out-of-band download. Use download-bytes for authenticated byte reads such as mail attachments, profile photos, Teams hosted content, and meeting recordings. Both tools take relative Microsoft Graph paths, not absolute URLs. For uploads, upload-file-content takes a base64 string body up to 4MB; use create-upload-session above that."
9
+ "Files / binary content: for large drive/SharePoint file content, prefer get-download-url to resolve a pre-authenticated URL for out-of-band download. Use download-bytes for authenticated byte reads such as mail attachments, profile photos, Teams hosted content, and meeting recordings. In stdio mode, download-bytes-to-file writes those same authenticated bytes straight to a local absolute path instead of returning base64 \u2014 the only out-of-band option for large mail attachments and meeting recordings, which get-download-url cannot handle. These tools take relative Microsoft Graph paths, not absolute URLs. For uploads, upload-file-content takes a base64 string body up to 4MB; use create-upload-session above that."
10
10
  ];
11
11
  if (opts.readOnly) parts.push("This server is read-only; write operations are disabled.");
12
12
  if (opts.multiAccount)
package/dist/server.js CHANGED
@@ -88,7 +88,8 @@ class MicrosoftGraphServer {
88
88
  this.multiAccount,
89
89
  this.accountNames,
90
90
  this.options.enabledTools,
91
- this.options.allowedScopes
91
+ this.options.allowedScopes,
92
+ Boolean(this.options.http)
92
93
  );
93
94
  } else {
94
95
  registerGraphTools(
@@ -100,7 +101,8 @@ class MicrosoftGraphServer {
100
101
  this.authManager,
101
102
  this.multiAccount,
102
103
  this.accountNames,
103
- this.options.allowedScopes
104
+ this.options.allowedScopes,
105
+ Boolean(this.options.http)
104
106
  );
105
107
  }
106
108
  installToolSchemaRefNormalization(server);
@@ -53,13 +53,32 @@ const PRESET_META = {
53
53
  requiresOrgMode: true
54
54
  }
55
55
  };
56
+ const UNIVERSAL_UTILITY_TOOLS = ["download-bytes", "download-bytes-to-file"];
57
+ const SCOPED_UTILITY_TOOLS = {
58
+ "get-download-url": ["files", "onedrive", "personal", "work", "search"],
59
+ "parse-teams-url": ["teams", "work"]
60
+ };
61
+ for (const [tool, presets] of Object.entries(SCOPED_UTILITY_TOOLS)) {
62
+ for (const preset of presets) {
63
+ if (!Object.prototype.hasOwnProperty.call(PRESET_META, preset)) {
64
+ throw new Error(
65
+ `SCOPED_UTILITY_TOOLS["${tool}"] references unknown preset "${preset}" (not in PRESET_META)`
66
+ );
67
+ }
68
+ }
69
+ }
56
70
  function presetPattern(preset) {
57
- const names = [
71
+ const endpointNames = [
58
72
  ...new Set(endpointEntries.filter((e) => e.presets?.includes(preset)).map((e) => e.toolName))
59
73
  ];
60
- if (names.length === 0) {
74
+ if (endpointNames.length === 0) {
61
75
  throw new Error(`Preset "${preset}" matches no endpoints in endpoints.json`);
62
76
  }
77
+ const names = [
78
+ ...endpointNames,
79
+ ...UNIVERSAL_UTILITY_TOOLS,
80
+ ...Object.entries(SCOPED_UTILITY_TOOLS).filter(([, presets]) => presets.includes(preset)).map(([name]) => name)
81
+ ];
63
82
  return new RegExp(`^(?:${names.join("|")})$`);
64
83
  }
65
84
  const TOOL_CATEGORIES = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@softeria/ms-365-mcp-server",
3
- "version": "0.132.0",
3
+ "version": "0.133.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",