@softeria/ms-365-mcp-server 0.25.0 → 0.27.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.
@@ -4,11 +4,194 @@ import { z } from "zod";
4
4
  import { readFileSync } from "fs";
5
5
  import path from "path";
6
6
  import { fileURLToPath } from "url";
7
+ import { TOOL_CATEGORIES } from "./tool-categories.js";
7
8
  const __filename = fileURLToPath(import.meta.url);
8
9
  const __dirname = path.dirname(__filename);
9
10
  const endpointsData = JSON.parse(
10
11
  readFileSync(path.join(__dirname, "endpoints.json"), "utf8")
11
12
  );
13
+ async function executeGraphTool(tool, config, graphClient, params) {
14
+ logger.info(`Tool ${tool.alias} called with params: ${JSON.stringify(params)}`);
15
+ try {
16
+ const parameterDefinitions = tool.parameters || [];
17
+ let path2 = tool.path;
18
+ const queryParams = {};
19
+ const headers = {};
20
+ let body = null;
21
+ for (const [paramName, paramValue] of Object.entries(params)) {
22
+ if (["fetchAllPages", "includeHeaders", "excludeResponse", "timezone"].includes(paramName)) {
23
+ continue;
24
+ }
25
+ const odataParams = [
26
+ "filter",
27
+ "select",
28
+ "expand",
29
+ "orderby",
30
+ "skip",
31
+ "top",
32
+ "count",
33
+ "search",
34
+ "format"
35
+ ];
36
+ const normalizedParamName = paramName.startsWith("$") ? paramName.slice(1) : paramName;
37
+ const isOdataParam = odataParams.includes(normalizedParamName.toLowerCase());
38
+ const fixedParamName = isOdataParam ? `$${normalizedParamName.toLowerCase()}` : paramName;
39
+ const paramDef = parameterDefinitions.find(
40
+ (p) => p.name === paramName || isOdataParam && p.name === normalizedParamName
41
+ );
42
+ if (paramDef) {
43
+ switch (paramDef.type) {
44
+ case "Path":
45
+ path2 = path2.replace(`{${paramName}}`, encodeURIComponent(paramValue)).replace(`:${paramName}`, encodeURIComponent(paramValue));
46
+ break;
47
+ case "Query":
48
+ queryParams[fixedParamName] = `${paramValue}`;
49
+ break;
50
+ case "Body":
51
+ if (paramDef.schema) {
52
+ const parseResult = paramDef.schema.safeParse(paramValue);
53
+ if (!parseResult.success) {
54
+ const wrapped = { [paramName]: paramValue };
55
+ const wrappedResult = paramDef.schema.safeParse(wrapped);
56
+ if (wrappedResult.success) {
57
+ logger.info(
58
+ `Auto-corrected parameter '${paramName}': AI passed nested field directly, wrapped it as {${paramName}: ...}`
59
+ );
60
+ body = wrapped;
61
+ } else {
62
+ body = paramValue;
63
+ }
64
+ } else {
65
+ body = paramValue;
66
+ }
67
+ } else {
68
+ body = paramValue;
69
+ }
70
+ break;
71
+ case "Header":
72
+ headers[fixedParamName] = `${paramValue}`;
73
+ break;
74
+ }
75
+ } else if (paramName === "body") {
76
+ body = paramValue;
77
+ logger.info(`Set body param: ${JSON.stringify(body)}`);
78
+ }
79
+ }
80
+ if (config?.supportsTimezone && params.timezone) {
81
+ headers["Prefer"] = `outlook.timezone="${params.timezone}"`;
82
+ logger.info(`Setting timezone header: Prefer: outlook.timezone="${params.timezone}"`);
83
+ }
84
+ if (Object.keys(queryParams).length > 0) {
85
+ const queryString = Object.entries(queryParams).map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`).join("&");
86
+ path2 = `${path2}${path2.includes("?") ? "&" : "?"}${queryString}`;
87
+ }
88
+ const options = {
89
+ method: tool.method.toUpperCase(),
90
+ headers
91
+ };
92
+ if (options.method !== "GET" && body) {
93
+ options.body = typeof body === "string" ? body : JSON.stringify(body);
94
+ }
95
+ const isProbablyMediaContent = tool.errors?.some((error) => error.description === "Retrieved media content") || path2.endsWith("/content");
96
+ if (config?.returnDownloadUrl && path2.endsWith("/content")) {
97
+ path2 = path2.replace(/\/content$/, "");
98
+ logger.info(
99
+ `Auto-returning download URL for ${tool.alias} (returnDownloadUrl=true in endpoints.json)`
100
+ );
101
+ } else if (isProbablyMediaContent) {
102
+ options.rawResponse = true;
103
+ }
104
+ if (params.includeHeaders === true) {
105
+ options.includeHeaders = true;
106
+ }
107
+ if (params.excludeResponse === true) {
108
+ options.excludeResponse = true;
109
+ }
110
+ logger.info(`Making graph request to ${path2} with options: ${JSON.stringify(options)}`);
111
+ let response = await graphClient.graphRequest(path2, options);
112
+ const fetchAllPages = params.fetchAllPages === true;
113
+ if (fetchAllPages && response?.content?.[0]?.text) {
114
+ try {
115
+ let combinedResponse = JSON.parse(response.content[0].text);
116
+ let allItems = combinedResponse.value || [];
117
+ let nextLink = combinedResponse["@odata.nextLink"];
118
+ let pageCount = 1;
119
+ while (nextLink && pageCount < 100) {
120
+ logger.info(`Fetching page ${pageCount + 1} from: ${nextLink}`);
121
+ const url = new URL(nextLink);
122
+ const nextPath = url.pathname.replace("/v1.0", "");
123
+ const nextOptions = { ...options };
124
+ const nextQueryParams = {};
125
+ for (const [key, value] of url.searchParams.entries()) {
126
+ nextQueryParams[key] = value;
127
+ }
128
+ nextOptions.queryParams = nextQueryParams;
129
+ const nextResponse = await graphClient.graphRequest(nextPath, nextOptions);
130
+ if (nextResponse?.content?.[0]?.text) {
131
+ const nextJsonResponse = JSON.parse(nextResponse.content[0].text);
132
+ if (nextJsonResponse.value && Array.isArray(nextJsonResponse.value)) {
133
+ allItems = allItems.concat(nextJsonResponse.value);
134
+ }
135
+ nextLink = nextJsonResponse["@odata.nextLink"];
136
+ pageCount++;
137
+ } else {
138
+ break;
139
+ }
140
+ }
141
+ if (pageCount >= 100) {
142
+ logger.warn(`Reached maximum page limit (100) for pagination`);
143
+ }
144
+ combinedResponse.value = allItems;
145
+ if (combinedResponse["@odata.count"]) {
146
+ combinedResponse["@odata.count"] = allItems.length;
147
+ }
148
+ delete combinedResponse["@odata.nextLink"];
149
+ response.content[0].text = JSON.stringify(combinedResponse);
150
+ logger.info(
151
+ `Pagination complete: collected ${allItems.length} items across ${pageCount} pages`
152
+ );
153
+ } catch (e) {
154
+ logger.error(`Error during pagination: ${e}`);
155
+ }
156
+ }
157
+ if (response?.content?.[0]?.text) {
158
+ const responseText = response.content[0].text;
159
+ logger.info(`Response size: ${responseText.length} characters`);
160
+ try {
161
+ const jsonResponse = JSON.parse(responseText);
162
+ if (jsonResponse.value && Array.isArray(jsonResponse.value)) {
163
+ logger.info(`Response contains ${jsonResponse.value.length} items`);
164
+ }
165
+ if (jsonResponse["@odata.nextLink"]) {
166
+ logger.info(`Response has pagination nextLink: ${jsonResponse["@odata.nextLink"]}`);
167
+ }
168
+ } catch {
169
+ }
170
+ }
171
+ const content = response.content.map((item) => ({
172
+ type: "text",
173
+ text: item.text
174
+ }));
175
+ return {
176
+ content,
177
+ _meta: response._meta,
178
+ isError: response.isError
179
+ };
180
+ } catch (error) {
181
+ logger.error(`Error in tool ${tool.alias}: ${error.message}`);
182
+ return {
183
+ content: [
184
+ {
185
+ type: "text",
186
+ text: JSON.stringify({
187
+ error: `Error in tool ${tool.alias}: ${error.message}`
188
+ })
189
+ }
190
+ ],
191
+ isError: true
192
+ };
193
+ }
194
+ }
12
195
  function registerGraphTools(server, graphClient, readOnly = false, enabledToolsPattern, orgMode = false) {
13
196
  let enabledToolsRegex;
14
197
  if (enabledToolsPattern) {
@@ -19,18 +202,24 @@ function registerGraphTools(server, graphClient, readOnly = false, enabledToolsP
19
202
  logger.error(`Invalid tool filter regex pattern: ${enabledToolsPattern}. Ignoring filter.`);
20
203
  }
21
204
  }
205
+ let registeredCount = 0;
206
+ let skippedCount = 0;
207
+ let failedCount = 0;
22
208
  for (const tool of api.endpoints) {
23
209
  const endpointConfig = endpointsData.find((e) => e.toolName === tool.alias);
24
210
  if (!orgMode && endpointConfig && !endpointConfig.scopes && endpointConfig.workScopes) {
25
211
  logger.info(`Skipping work account tool ${tool.alias} - not in org mode`);
212
+ skippedCount++;
26
213
  continue;
27
214
  }
28
215
  if (readOnly && tool.method.toUpperCase() !== "GET") {
29
216
  logger.info(`Skipping write operation ${tool.alias} in read-only mode`);
217
+ skippedCount++;
30
218
  continue;
31
219
  }
32
220
  if (enabledToolsRegex && !enabledToolsRegex.test(tool.alias)) {
33
221
  logger.info(`Skipping tool ${tool.alias} - doesn't match filter pattern`);
222
+ skippedCount++;
34
223
  continue;
35
224
  }
36
225
  const paramSchema = {};
@@ -44,220 +233,144 @@ function registerGraphTools(server, graphClient, readOnly = false, enabledToolsP
44
233
  }
45
234
  paramSchema["includeHeaders"] = z.boolean().describe("Include response headers (including ETag) in the response metadata").optional();
46
235
  paramSchema["excludeResponse"] = z.boolean().describe("Exclude the full response body and only return success or failure indication").optional();
236
+ if (endpointConfig?.supportsTimezone) {
237
+ paramSchema["timezone"] = z.string().describe(
238
+ 'IANA timezone name (e.g., "America/New_York", "Europe/London", "Asia/Tokyo") for calendar event times. If not specified, times are returned in UTC.'
239
+ ).optional();
240
+ }
47
241
  let toolDescription = tool.description || `Execute ${tool.method.toUpperCase()} request to ${tool.path}`;
48
242
  if (endpointConfig?.llmTip) {
49
243
  toolDescription += `
50
244
 
51
245
  \u{1F4A1} TIP: ${endpointConfig.llmTip}`;
52
246
  }
53
- server.tool(
54
- tool.alias,
55
- toolDescription,
56
- paramSchema,
57
- {
58
- title: tool.alias,
59
- readOnlyHint: tool.method.toUpperCase() === "GET"
60
- },
61
- async (params) => {
62
- logger.info(`Tool ${tool.alias} called with params: ${JSON.stringify(params)}`);
63
- try {
64
- logger.info(`params: ${JSON.stringify(params)}`);
65
- const parameterDefinitions = tool.parameters || [];
66
- let path2 = tool.path;
67
- const queryParams = {};
68
- const headers = {};
69
- let body = null;
70
- for (let [paramName, paramValue] of Object.entries(params)) {
71
- if (paramName === "fetchAllPages") {
72
- continue;
73
- }
74
- if (paramName === "includeHeaders") {
75
- continue;
76
- }
77
- if (paramName === "excludeResponse") {
78
- continue;
79
- }
80
- const odataParams = [
81
- "filter",
82
- "select",
83
- "expand",
84
- "orderby",
85
- "skip",
86
- "top",
87
- "count",
88
- "search",
89
- "format"
90
- ];
91
- const fixedParamName = odataParams.includes(paramName.toLowerCase()) ? `$${paramName.toLowerCase()}` : paramName;
92
- const paramDef = parameterDefinitions.find((p) => p.name === paramName);
93
- if (paramDef) {
94
- switch (paramDef.type) {
95
- case "Path":
96
- path2 = path2.replace(`{${paramName}}`, encodeURIComponent(paramValue)).replace(`:${paramName}`, encodeURIComponent(paramValue));
97
- break;
98
- case "Query":
99
- queryParams[fixedParamName] = `${paramValue}`;
100
- break;
101
- case "Body":
102
- if (paramDef.schema) {
103
- const parseResult = paramDef.schema.safeParse(paramValue);
104
- if (!parseResult.success) {
105
- const wrapped = { [paramName]: paramValue };
106
- const wrappedResult = paramDef.schema.safeParse(wrapped);
107
- if (wrappedResult.success) {
108
- logger.info(
109
- `Auto-corrected parameter '${paramName}': AI passed nested field directly, wrapped it as {${paramName}: ...}`
110
- );
111
- body = wrapped;
112
- } else {
113
- body = paramValue;
114
- }
115
- } else {
116
- body = paramValue;
117
- }
118
- } else {
119
- body = paramValue;
120
- }
121
- break;
122
- case "Header":
123
- headers[fixedParamName] = `${paramValue}`;
124
- break;
125
- }
126
- } else if (paramName === "body") {
127
- body = paramValue;
128
- logger.info(`Set body param: ${JSON.stringify(body)}`);
129
- }
130
- }
131
- if (Object.keys(queryParams).length > 0) {
132
- const queryString = Object.entries(queryParams).map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`).join("&");
133
- path2 = `${path2}${path2.includes("?") ? "&" : "?"}${queryString}`;
134
- }
135
- const options = {
136
- method: tool.method.toUpperCase(),
137
- headers
138
- };
139
- if (options.method !== "GET" && body) {
140
- options.body = typeof body === "string" ? body : JSON.stringify(body);
141
- }
142
- const isProbablyMediaContent = tool.errors?.some((error) => error.description === "Retrieved media content") || path2.endsWith("/content");
143
- if (endpointConfig?.returnDownloadUrl && path2.endsWith("/content")) {
144
- path2 = path2.replace(/\/content$/, "");
145
- logger.info(
146
- `Auto-returning download URL for ${tool.alias} (returnDownloadUrl=true in endpoints.json)`
147
- );
148
- } else if (isProbablyMediaContent) {
149
- options.rawResponse = true;
150
- }
151
- if (params.includeHeaders === true) {
152
- options.includeHeaders = true;
153
- }
154
- if (params.excludeResponse === true) {
155
- options.excludeResponse = true;
156
- }
157
- logger.info(`Making graph request to ${path2} with options: ${JSON.stringify(options)}`);
158
- let response = await graphClient.graphRequest(path2, options);
159
- const fetchAllPages = params.fetchAllPages === true;
160
- if (fetchAllPages && response && response.content && response.content.length > 0) {
161
- try {
162
- let combinedResponse = JSON.parse(response.content[0].text);
163
- let allItems = combinedResponse.value || [];
164
- let nextLink = combinedResponse["@odata.nextLink"];
165
- let pageCount = 1;
166
- while (nextLink) {
167
- logger.info(`Fetching page ${pageCount + 1} from: ${nextLink}`);
168
- const url = new URL(nextLink);
169
- const nextPath = url.pathname.replace("/v1.0", "");
170
- const nextOptions = { ...options };
171
- const nextQueryParams = {};
172
- for (const [key, value] of url.searchParams.entries()) {
173
- nextQueryParams[key] = value;
174
- }
175
- nextOptions.queryParams = nextQueryParams;
176
- const nextResponse = await graphClient.graphRequest(nextPath, nextOptions);
177
- if (nextResponse && nextResponse.content && nextResponse.content.length > 0) {
178
- const nextJsonResponse = JSON.parse(nextResponse.content[0].text);
179
- if (nextJsonResponse.value && Array.isArray(nextJsonResponse.value)) {
180
- allItems = allItems.concat(nextJsonResponse.value);
181
- }
182
- nextLink = nextJsonResponse["@odata.nextLink"];
183
- pageCount++;
184
- if (pageCount > 100) {
185
- logger.warn(`Reached maximum page limit (100) for pagination`);
186
- break;
187
- }
188
- } else {
189
- break;
190
- }
191
- }
192
- combinedResponse.value = allItems;
193
- if (combinedResponse["@odata.count"]) {
194
- combinedResponse["@odata.count"] = allItems.length;
195
- }
196
- delete combinedResponse["@odata.nextLink"];
197
- response.content[0].text = JSON.stringify(combinedResponse);
198
- logger.info(
199
- `Pagination complete: collected ${allItems.length} items across ${pageCount} pages`
200
- );
201
- } catch (e) {
202
- logger.error(`Error during pagination: ${e}`);
203
- }
247
+ try {
248
+ server.tool(
249
+ tool.alias,
250
+ toolDescription,
251
+ paramSchema,
252
+ {
253
+ title: tool.alias,
254
+ readOnlyHint: tool.method.toUpperCase() === "GET"
255
+ },
256
+ async (params) => executeGraphTool(tool, endpointConfig, graphClient, params)
257
+ );
258
+ registeredCount++;
259
+ } catch (error) {
260
+ logger.error(`Failed to register tool ${tool.alias}: ${error.message}`);
261
+ failedCount++;
262
+ }
263
+ }
264
+ logger.info(
265
+ `Tool registration complete: ${registeredCount} registered, ${skippedCount} skipped, ${failedCount} failed`
266
+ );
267
+ return registeredCount;
268
+ }
269
+ function buildToolsRegistry(readOnly, orgMode) {
270
+ const toolsMap = /* @__PURE__ */ new Map();
271
+ for (const tool of api.endpoints) {
272
+ const endpointConfig = endpointsData.find((e) => e.toolName === tool.alias);
273
+ if (!orgMode && endpointConfig && !endpointConfig.scopes && endpointConfig.workScopes) {
274
+ continue;
275
+ }
276
+ if (readOnly && tool.method.toUpperCase() !== "GET") {
277
+ continue;
278
+ }
279
+ toolsMap.set(tool.alias, { tool, config: endpointConfig });
280
+ }
281
+ return toolsMap;
282
+ }
283
+ function registerDiscoveryTools(server, graphClient, readOnly = false, orgMode = false) {
284
+ const toolsRegistry = buildToolsRegistry(readOnly, orgMode);
285
+ logger.info(`Discovery mode: ${toolsRegistry.size} tools available in registry`);
286
+ server.tool(
287
+ "search-tools",
288
+ `Search through ${toolsRegistry.size} available Microsoft Graph API tools. Use this to find tools by name, path, or description before executing them.`,
289
+ {
290
+ query: z.string().describe("Search query to filter tools (searches name, path, and description)").optional(),
291
+ category: z.string().describe(
292
+ "Filter by category: mail, calendar, files, contacts, tasks, onenote, search, users, excel"
293
+ ).optional(),
294
+ limit: z.number().describe("Maximum results to return (default: 20, max: 50)").optional()
295
+ },
296
+ {
297
+ title: "search-tools",
298
+ readOnlyHint: true
299
+ },
300
+ async ({ query, category, limit = 20 }) => {
301
+ const maxLimit = Math.min(limit, 50);
302
+ const results = [];
303
+ const queryLower = query?.toLowerCase();
304
+ const categoryDef = category ? TOOL_CATEGORIES[category] : void 0;
305
+ for (const [name, { tool, config }] of toolsRegistry) {
306
+ if (categoryDef && !categoryDef.pattern.test(name)) {
307
+ continue;
308
+ }
309
+ if (queryLower) {
310
+ const searchText = `${name} ${tool.path} ${tool.description || ""} ${config?.llmTip || ""}`.toLowerCase();
311
+ if (!searchText.includes(queryLower)) {
312
+ continue;
204
313
  }
205
- if (response && response.content && response.content.length > 0) {
206
- const responseText = response.content[0].text;
207
- const responseSize = responseText.length;
208
- logger.info(`Response size: ${responseSize} characters`);
209
- try {
210
- const jsonResponse = JSON.parse(responseText);
211
- if (jsonResponse.value && Array.isArray(jsonResponse.value)) {
212
- logger.info(`Response contains ${jsonResponse.value.length} items`);
213
- if (jsonResponse.value.length > 0 && jsonResponse.value[0].body) {
214
- logger.info(
215
- `First item has body field with size: ${JSON.stringify(jsonResponse.value[0].body).length} characters`
216
- );
217
- }
218
- }
219
- if (jsonResponse["@odata.nextLink"]) {
220
- logger.info(`Response has pagination nextLink: ${jsonResponse["@odata.nextLink"]}`);
221
- }
222
- const preview = responseText.substring(0, 500);
223
- logger.info(`Response preview: ${preview}${responseText.length > 500 ? "..." : ""}`);
224
- } catch {
225
- const preview = responseText.substring(0, 500);
226
- logger.info(
227
- `Response preview (non-JSON): ${preview}${responseText.length > 500 ? "..." : ""}`
228
- );
229
- }
314
+ }
315
+ results.push({
316
+ name,
317
+ method: tool.method.toUpperCase(),
318
+ path: tool.path,
319
+ description: tool.description || `${tool.method.toUpperCase()} ${tool.path}`
320
+ });
321
+ if (results.length >= maxLimit) break;
322
+ }
323
+ return {
324
+ content: [
325
+ {
326
+ type: "text",
327
+ text: JSON.stringify(
328
+ {
329
+ found: results.length,
330
+ total: toolsRegistry.size,
331
+ tools: results,
332
+ tip: "Use execute-tool with the tool name and required parameters to call any of these tools."
333
+ },
334
+ null,
335
+ 2
336
+ )
230
337
  }
231
- const content = response.content.map((item) => {
232
- const textContent = {
338
+ ]
339
+ };
340
+ }
341
+ );
342
+ server.tool(
343
+ "execute-tool",
344
+ "Execute a Microsoft Graph API tool by name. Use search-tools first to find available tools and their parameters.",
345
+ {
346
+ tool_name: z.string().describe('Name of the tool to execute (e.g., "list-mail-messages")'),
347
+ parameters: z.record(z.any()).describe("Parameters to pass to the tool as key-value pairs").optional()
348
+ },
349
+ {
350
+ title: "execute-tool",
351
+ readOnlyHint: false
352
+ },
353
+ async ({ tool_name, parameters = {} }) => {
354
+ const toolData = toolsRegistry.get(tool_name);
355
+ if (!toolData) {
356
+ return {
357
+ content: [
358
+ {
233
359
  type: "text",
234
- text: item.text
235
- };
236
- return textContent;
237
- });
238
- const result = {
239
- content,
240
- _meta: response._meta,
241
- isError: response.isError
242
- };
243
- return result;
244
- } catch (error) {
245
- logger.error(`Error in tool ${tool.alias}: ${error.message}`);
246
- const errorContent = {
247
- type: "text",
248
- text: JSON.stringify({
249
- error: `Error in tool ${tool.alias}: ${error.message}`
250
- })
251
- };
252
- return {
253
- content: [errorContent],
254
- isError: true
255
- };
256
- }
360
+ text: JSON.stringify({
361
+ error: `Tool not found: ${tool_name}`,
362
+ tip: "Use search-tools to find available tools."
363
+ })
364
+ }
365
+ ],
366
+ isError: true
367
+ };
257
368
  }
258
- );
259
- }
369
+ return executeGraphTool(toolData.tool, toolData.config, graphClient, parameters);
370
+ }
371
+ );
260
372
  }
261
373
  export {
374
+ registerDiscoveryTools,
262
375
  registerGraphTools
263
376
  };
package/dist/server.js CHANGED
@@ -6,7 +6,7 @@ import express from "express";
6
6
  import crypto from "crypto";
7
7
  import logger, { enableConsoleLogging } from "./logger.js";
8
8
  import { registerAuthTools } from "./auth-tools.js";
9
- import { registerGraphTools } from "./graph-tools.js";
9
+ import { registerGraphTools, registerDiscoveryTools } from "./graph-tools.js";
10
10
  import GraphClient from "./graph-client.js";
11
11
  import { buildScopesFromEndpoints } from "./auth.js";
12
12
  import { MicrosoftOAuthProvider } from "./oauth-provider.js";
@@ -33,13 +33,23 @@ class MicrosoftGraphServer {
33
33
  if (shouldRegisterAuthTools) {
34
34
  registerAuthTools(this.server, this.authManager);
35
35
  }
36
- registerGraphTools(
37
- this.server,
38
- this.graphClient,
39
- this.options.readOnly,
40
- this.options.enabledTools,
41
- this.options.orgMode
42
- );
36
+ if (this.options.discovery) {
37
+ logger.info("Discovery mode enabled (experimental) - registering discovery tool only");
38
+ registerDiscoveryTools(
39
+ this.server,
40
+ this.graphClient,
41
+ this.options.readOnly,
42
+ this.options.orgMode
43
+ );
44
+ } else {
45
+ registerGraphTools(
46
+ this.server,
47
+ this.graphClient,
48
+ this.options.readOnly,
49
+ this.options.enabledTools,
50
+ this.options.orgMode
51
+ );
52
+ }
43
53
  }
44
54
  async start() {
45
55
  if (this.options.v) {