@dashflow/ms365-mcp-server 1.0.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.
@@ -0,0 +1,42 @@
1
+ import { z } from "zod";
2
+ function makeApi(endpoints) {
3
+ return endpoints;
4
+ }
5
+ class Zodios {
6
+ constructor(baseUrlOrEndpoints, endpoints, options) {
7
+ if (typeof baseUrlOrEndpoints === "string") {
8
+ throw new Error("No such hack");
9
+ }
10
+ this.endpoints = baseUrlOrEndpoints.map((endpoint) => {
11
+ endpoint.parameters = endpoint.parameters || [];
12
+ for (const parameter of endpoint.parameters) {
13
+ parameter.name = parameter.name.replace(/[$_]+/g, "");
14
+ }
15
+ const pathParamRegex = /:([a-zA-Z0-9]+)/g;
16
+ const pathParams = [];
17
+ let match;
18
+ while ((match = pathParamRegex.exec(endpoint.path)) !== null) {
19
+ pathParams.push(match[1]);
20
+ }
21
+ for (const pathParam of pathParams) {
22
+ const paramExists = endpoint.parameters.some(
23
+ (param) => param.name === pathParam || param.name === pathParam.replace(/[$_]+/g, "")
24
+ );
25
+ if (!paramExists) {
26
+ const newParam = {
27
+ name: pathParam,
28
+ type: "Path",
29
+ schema: z.string().describe(`Path parameter: ${pathParam}`),
30
+ description: `Path parameter: ${pathParam}`
31
+ };
32
+ endpoint.parameters.push(newParam);
33
+ }
34
+ }
35
+ return endpoint;
36
+ });
37
+ }
38
+ }
39
+ export {
40
+ Zodios,
41
+ makeApi
42
+ };
@@ -0,0 +1,208 @@
1
+ import logger from "./logger.js";
2
+ import { refreshAccessToken } from "./lib/microsoft-auth.js";
3
+ import { encode as toonEncode } from "@toon-format/toon";
4
+ import { getCloudEndpoints } from "./cloud-config.js";
5
+ import { getRequestTokens } from "./request-context.js";
6
+ class GraphClient {
7
+ constructor(authManager, secrets, outputFormat = "json") {
8
+ this.outputFormat = "json";
9
+ this.authManager = authManager;
10
+ this.secrets = secrets;
11
+ this.outputFormat = outputFormat;
12
+ }
13
+ async makeRequest(endpoint, options = {}) {
14
+ const contextTokens = getRequestTokens();
15
+ let accessToken = options.accessToken ?? contextTokens?.accessToken ?? await this.authManager.getToken();
16
+ const refreshToken = options.refreshToken ?? contextTokens?.refreshToken;
17
+ if (!accessToken) {
18
+ throw new Error("No access token available");
19
+ }
20
+ try {
21
+ let response = await this.performRequest(endpoint, accessToken, options);
22
+ if (response.status === 401 && refreshToken) {
23
+ const newTokens = await this.refreshAccessToken(refreshToken);
24
+ accessToken = newTokens.accessToken;
25
+ response = await this.performRequest(endpoint, accessToken, options);
26
+ }
27
+ if (response.status === 403) {
28
+ const errorText = await response.text();
29
+ if (errorText.includes("scope") || errorText.includes("permission")) {
30
+ throw new Error(
31
+ `Microsoft Graph API scope error: ${response.status} ${response.statusText} - ${errorText}. This tool requires organization mode. Please restart with --org-mode flag.`
32
+ );
33
+ }
34
+ throw new Error(
35
+ `Microsoft Graph API error: ${response.status} ${response.statusText} - ${errorText}`
36
+ );
37
+ }
38
+ if (!response.ok) {
39
+ throw new Error(
40
+ `Microsoft Graph API error: ${response.status} ${response.statusText} - ${await response.text()}`
41
+ );
42
+ }
43
+ const text = await response.text();
44
+ let result;
45
+ if (text === "") {
46
+ result = { message: "OK!" };
47
+ } else {
48
+ try {
49
+ result = JSON.parse(text);
50
+ } catch {
51
+ result = { message: "OK!", rawResponse: text };
52
+ }
53
+ }
54
+ if (options.includeHeaders) {
55
+ const etag = response.headers.get("ETag") || response.headers.get("etag");
56
+ if (result && typeof result === "object" && !Array.isArray(result)) {
57
+ return {
58
+ ...result,
59
+ _etag: etag || "no-etag-found"
60
+ };
61
+ }
62
+ }
63
+ return result;
64
+ } catch (error) {
65
+ logger.error("Microsoft Graph API request failed:", error);
66
+ throw error;
67
+ }
68
+ }
69
+ async refreshAccessToken(refreshToken) {
70
+ const tenantId = this.secrets.tenantId || "common";
71
+ const clientId = this.secrets.clientId;
72
+ const clientSecret = this.secrets.clientSecret;
73
+ if (clientSecret) {
74
+ logger.info("GraphClient: Refreshing token with confidential client");
75
+ } else {
76
+ logger.info("GraphClient: Refreshing token with public client");
77
+ }
78
+ const response = await refreshAccessToken(
79
+ refreshToken,
80
+ clientId,
81
+ clientSecret,
82
+ tenantId,
83
+ this.secrets.cloudType
84
+ );
85
+ return {
86
+ accessToken: response.access_token,
87
+ refreshToken: response.refresh_token
88
+ };
89
+ }
90
+ async performRequest(endpoint, accessToken, options) {
91
+ const cloudEndpoints = getCloudEndpoints(this.secrets.cloudType);
92
+ const url = `${cloudEndpoints.graphApi}/v1.0${endpoint}`;
93
+ const headers = {
94
+ Authorization: `Bearer ${accessToken}`,
95
+ "Content-Type": "application/json",
96
+ ...options.headers
97
+ };
98
+ return fetch(url, {
99
+ method: options.method || "GET",
100
+ headers,
101
+ body: options.body
102
+ });
103
+ }
104
+ serializeData(data, outputFormat, pretty = false) {
105
+ if (outputFormat === "toon") {
106
+ try {
107
+ return toonEncode(data);
108
+ } catch (error) {
109
+ logger.warn(`Failed to encode as TOON, falling back to JSON: ${error}`);
110
+ return JSON.stringify(data, null, pretty ? 2 : void 0);
111
+ }
112
+ }
113
+ return JSON.stringify(data, null, pretty ? 2 : void 0);
114
+ }
115
+ async graphRequest(endpoint, options = {}) {
116
+ try {
117
+ logger.info(`Calling ${endpoint} with options: ${JSON.stringify(options)}`);
118
+ const result = await this.makeRequest(endpoint, options);
119
+ return this.formatJsonResponse(result, options.rawResponse, options.excludeResponse);
120
+ } catch (error) {
121
+ logger.error(`Error in Graph API request: ${error}`);
122
+ return {
123
+ content: [{ type: "text", text: JSON.stringify({ error: error.message }) }],
124
+ isError: true
125
+ };
126
+ }
127
+ }
128
+ formatJsonResponse(data, rawResponse = false, excludeResponse = false) {
129
+ if (excludeResponse) {
130
+ return {
131
+ content: [{ type: "text", text: this.serializeData({ success: true }, this.outputFormat) }]
132
+ };
133
+ }
134
+ if (data && typeof data === "object" && "_headers" in data) {
135
+ const responseData = data;
136
+ const meta = {};
137
+ if (responseData._etag) {
138
+ meta.etag = responseData._etag;
139
+ }
140
+ if (responseData._headers) {
141
+ meta.headers = responseData._headers;
142
+ }
143
+ if (rawResponse) {
144
+ return {
145
+ content: [
146
+ { type: "text", text: this.serializeData(responseData.data, this.outputFormat) }
147
+ ],
148
+ _meta: meta
149
+ };
150
+ }
151
+ if (responseData.data === null || responseData.data === void 0) {
152
+ return {
153
+ content: [
154
+ { type: "text", text: this.serializeData({ success: true }, this.outputFormat) }
155
+ ],
156
+ _meta: meta
157
+ };
158
+ }
159
+ const removeODataProps2 = (obj) => {
160
+ if (typeof obj === "object" && obj !== null) {
161
+ Object.keys(obj).forEach((key) => {
162
+ if (key.startsWith("@odata.")) {
163
+ delete obj[key];
164
+ } else if (typeof obj[key] === "object") {
165
+ removeODataProps2(obj[key]);
166
+ }
167
+ });
168
+ }
169
+ };
170
+ removeODataProps2(responseData.data);
171
+ return {
172
+ content: [
173
+ { type: "text", text: this.serializeData(responseData.data, this.outputFormat, true) }
174
+ ],
175
+ _meta: meta
176
+ };
177
+ }
178
+ if (rawResponse) {
179
+ return {
180
+ content: [{ type: "text", text: this.serializeData(data, this.outputFormat) }]
181
+ };
182
+ }
183
+ if (data === null || data === void 0) {
184
+ return {
185
+ content: [{ type: "text", text: this.serializeData({ success: true }, this.outputFormat) }]
186
+ };
187
+ }
188
+ const removeODataProps = (obj) => {
189
+ if (typeof obj === "object" && obj !== null) {
190
+ Object.keys(obj).forEach((key) => {
191
+ if (key.startsWith("@odata.")) {
192
+ delete obj[key];
193
+ } else if (typeof obj[key] === "object") {
194
+ removeODataProps(obj[key]);
195
+ }
196
+ });
197
+ }
198
+ };
199
+ removeODataProps(data);
200
+ return {
201
+ content: [{ type: "text", text: this.serializeData(data, this.outputFormat, true) }]
202
+ };
203
+ }
204
+ }
205
+ var graph_client_default = GraphClient;
206
+ export {
207
+ graph_client_default as default
208
+ };
@@ -0,0 +1,401 @@
1
+ import logger from "./logger.js";
2
+ import { api } from "./generated/client.js";
3
+ import { z } from "zod";
4
+ import { readFileSync } from "fs";
5
+ import path from "path";
6
+ import { fileURLToPath } from "url";
7
+ import { TOOL_CATEGORIES } from "./tool-categories.js";
8
+ const __filename = fileURLToPath(import.meta.url);
9
+ const __dirname = path.dirname(__filename);
10
+ const endpointsData = JSON.parse(
11
+ readFileSync(path.join(__dirname, "endpoints.json"), "utf8")
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
+ if (paramValue !== "" && paramValue != null) {
49
+ queryParams[fixedParamName] = `${paramValue}`;
50
+ }
51
+ break;
52
+ case "Body":
53
+ if (paramDef.schema) {
54
+ const parseResult = paramDef.schema.safeParse(paramValue);
55
+ if (!parseResult.success) {
56
+ const wrapped = { [paramName]: paramValue };
57
+ const wrappedResult = paramDef.schema.safeParse(wrapped);
58
+ if (wrappedResult.success) {
59
+ logger.info(
60
+ `Auto-corrected parameter '${paramName}': AI passed nested field directly, wrapped it as {${paramName}: ...}`
61
+ );
62
+ body = wrapped;
63
+ } else {
64
+ body = paramValue;
65
+ }
66
+ } else {
67
+ body = paramValue;
68
+ }
69
+ } else {
70
+ body = paramValue;
71
+ }
72
+ break;
73
+ case "Header":
74
+ headers[fixedParamName] = `${paramValue}`;
75
+ break;
76
+ }
77
+ } else if (paramName === "body") {
78
+ body = paramValue;
79
+ logger.info(`Set body param: ${JSON.stringify(body)}`);
80
+ }
81
+ }
82
+ if (config?.supportsTimezone && params.timezone) {
83
+ headers["Prefer"] = `outlook.timezone="${params.timezone}"`;
84
+ logger.info(`Setting timezone header: Prefer: outlook.timezone="${params.timezone}"`);
85
+ }
86
+ if (config?.contentType) {
87
+ headers["Content-Type"] = config.contentType;
88
+ logger.info(`Setting custom Content-Type: ${config.contentType}`);
89
+ }
90
+ if (Object.keys(queryParams).length > 0) {
91
+ const queryString = Object.entries(queryParams).map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`).join("&");
92
+ path2 = `${path2}${path2.includes("?") ? "&" : "?"}${queryString}`;
93
+ }
94
+ const options = {
95
+ method: tool.method.toUpperCase(),
96
+ headers
97
+ };
98
+ if (options.method !== "GET" && body) {
99
+ if (config?.contentType === "text/html") {
100
+ if (typeof body === "string") {
101
+ options.body = body;
102
+ } else if (typeof body === "object" && "content" in body) {
103
+ options.body = body.content;
104
+ } else {
105
+ options.body = String(body);
106
+ }
107
+ } else {
108
+ options.body = typeof body === "string" ? body : JSON.stringify(body);
109
+ }
110
+ }
111
+ const isProbablyMediaContent = tool.errors?.some((error) => error.description === "Retrieved media content") || path2.endsWith("/content");
112
+ if (config?.returnDownloadUrl && path2.endsWith("/content")) {
113
+ path2 = path2.replace(/\/content$/, "");
114
+ logger.info(
115
+ `Auto-returning download URL for ${tool.alias} (returnDownloadUrl=true in endpoints.json)`
116
+ );
117
+ } else if (isProbablyMediaContent) {
118
+ options.rawResponse = true;
119
+ }
120
+ if (params.includeHeaders === true) {
121
+ options.includeHeaders = true;
122
+ }
123
+ if (params.excludeResponse === true) {
124
+ options.excludeResponse = true;
125
+ }
126
+ logger.info(`Making graph request to ${path2} with options: ${JSON.stringify(options)}`);
127
+ let response = await graphClient.graphRequest(path2, options);
128
+ const fetchAllPages = params.fetchAllPages === true;
129
+ if (fetchAllPages && response?.content?.[0]?.text) {
130
+ try {
131
+ let combinedResponse = JSON.parse(response.content[0].text);
132
+ let allItems = combinedResponse.value || [];
133
+ let nextLink = combinedResponse["@odata.nextLink"];
134
+ let pageCount = 1;
135
+ while (nextLink && pageCount < 100) {
136
+ logger.info(`Fetching page ${pageCount + 1} from: ${nextLink}`);
137
+ const url = new URL(nextLink);
138
+ const nextPath = url.pathname.replace("/v1.0", "");
139
+ const nextOptions = { ...options };
140
+ const nextQueryParams = {};
141
+ for (const [key, value] of url.searchParams.entries()) {
142
+ nextQueryParams[key] = value;
143
+ }
144
+ nextOptions.queryParams = nextQueryParams;
145
+ const nextResponse = await graphClient.graphRequest(nextPath, nextOptions);
146
+ if (nextResponse?.content?.[0]?.text) {
147
+ const nextJsonResponse = JSON.parse(nextResponse.content[0].text);
148
+ if (nextJsonResponse.value && Array.isArray(nextJsonResponse.value)) {
149
+ allItems = allItems.concat(nextJsonResponse.value);
150
+ }
151
+ nextLink = nextJsonResponse["@odata.nextLink"];
152
+ pageCount++;
153
+ } else {
154
+ break;
155
+ }
156
+ }
157
+ if (pageCount >= 100) {
158
+ logger.warn(`Reached maximum page limit (100) for pagination`);
159
+ }
160
+ combinedResponse.value = allItems;
161
+ if (combinedResponse["@odata.count"]) {
162
+ combinedResponse["@odata.count"] = allItems.length;
163
+ }
164
+ delete combinedResponse["@odata.nextLink"];
165
+ response.content[0].text = JSON.stringify(combinedResponse);
166
+ logger.info(
167
+ `Pagination complete: collected ${allItems.length} items across ${pageCount} pages`
168
+ );
169
+ } catch (e) {
170
+ logger.error(`Error during pagination: ${e}`);
171
+ }
172
+ }
173
+ if (response?.content?.[0]?.text) {
174
+ const responseText = response.content[0].text;
175
+ logger.info(`Response size: ${responseText.length} characters`);
176
+ try {
177
+ const jsonResponse = JSON.parse(responseText);
178
+ if (jsonResponse.value && Array.isArray(jsonResponse.value)) {
179
+ logger.info(`Response contains ${jsonResponse.value.length} items`);
180
+ }
181
+ if (jsonResponse["@odata.nextLink"]) {
182
+ logger.info(`Response has pagination nextLink: ${jsonResponse["@odata.nextLink"]}`);
183
+ }
184
+ } catch {
185
+ }
186
+ }
187
+ const content = response.content.map((item) => ({
188
+ type: "text",
189
+ text: item.text
190
+ }));
191
+ return {
192
+ content,
193
+ _meta: response._meta,
194
+ isError: response.isError
195
+ };
196
+ } catch (error) {
197
+ logger.error(`Error in tool ${tool.alias}: ${error.message}`);
198
+ return {
199
+ content: [
200
+ {
201
+ type: "text",
202
+ text: JSON.stringify({
203
+ error: `Error in tool ${tool.alias}: ${error.message}`
204
+ })
205
+ }
206
+ ],
207
+ isError: true
208
+ };
209
+ }
210
+ }
211
+ function registerGraphTools(server, graphClient, readOnly = false, enabledToolsPattern, orgMode = false) {
212
+ let enabledToolsRegex;
213
+ if (enabledToolsPattern) {
214
+ try {
215
+ enabledToolsRegex = new RegExp(enabledToolsPattern, "i");
216
+ logger.info(`Tool filtering enabled with pattern: ${enabledToolsPattern}`);
217
+ } catch {
218
+ logger.error(`Invalid tool filter regex pattern: ${enabledToolsPattern}. Ignoring filter.`);
219
+ }
220
+ }
221
+ let registeredCount = 0;
222
+ let skippedCount = 0;
223
+ let failedCount = 0;
224
+ for (const tool of api.endpoints) {
225
+ const endpointConfig = endpointsData.find((e) => e.toolName === tool.alias);
226
+ if (!orgMode && endpointConfig && !endpointConfig.scopes && endpointConfig.workScopes) {
227
+ logger.info(`Skipping work account tool ${tool.alias} - not in org mode`);
228
+ skippedCount++;
229
+ continue;
230
+ }
231
+ if (readOnly && tool.method.toUpperCase() !== "GET") {
232
+ logger.info(`Skipping write operation ${tool.alias} in read-only mode`);
233
+ skippedCount++;
234
+ continue;
235
+ }
236
+ if (enabledToolsRegex && !enabledToolsRegex.test(tool.alias)) {
237
+ logger.info(`Skipping tool ${tool.alias} - doesn't match filter pattern`);
238
+ skippedCount++;
239
+ continue;
240
+ }
241
+ const paramSchema = {};
242
+ if (tool.parameters && tool.parameters.length > 0) {
243
+ for (const param of tool.parameters) {
244
+ paramSchema[param.name] = param.schema || z.any();
245
+ }
246
+ }
247
+ if (tool.method.toUpperCase() === "GET" && tool.path.includes("/")) {
248
+ paramSchema["fetchAllPages"] = z.boolean().describe("Automatically fetch all pages of results").optional();
249
+ }
250
+ paramSchema["includeHeaders"] = z.boolean().describe("Include response headers (including ETag) in the response metadata").optional();
251
+ paramSchema["excludeResponse"] = z.boolean().describe("Exclude the full response body and only return success or failure indication").optional();
252
+ if (endpointConfig?.supportsTimezone) {
253
+ paramSchema["timezone"] = z.string().describe(
254
+ 'IANA timezone name (e.g., "America/New_York", "Europe/London", "Asia/Tokyo") for calendar event times. If not specified, times are returned in UTC.'
255
+ ).optional();
256
+ }
257
+ let toolDescription = tool.description || `Execute ${tool.method.toUpperCase()} request to ${tool.path}`;
258
+ if (endpointConfig?.llmTip) {
259
+ toolDescription += `
260
+
261
+ \u{1F4A1} TIP: ${endpointConfig.llmTip}`;
262
+ }
263
+ try {
264
+ server.tool(
265
+ tool.alias,
266
+ toolDescription,
267
+ paramSchema,
268
+ {
269
+ title: tool.alias,
270
+ readOnlyHint: tool.method.toUpperCase() === "GET",
271
+ destructiveHint: ["POST", "PATCH", "DELETE"].includes(tool.method.toUpperCase()),
272
+ openWorldHint: true
273
+ // All tools call Microsoft Graph API
274
+ },
275
+ async (params) => executeGraphTool(tool, endpointConfig, graphClient, params)
276
+ );
277
+ registeredCount++;
278
+ } catch (error) {
279
+ logger.error(`Failed to register tool ${tool.alias}: ${error.message}`);
280
+ failedCount++;
281
+ }
282
+ }
283
+ logger.info(
284
+ `Tool registration complete: ${registeredCount} registered, ${skippedCount} skipped, ${failedCount} failed`
285
+ );
286
+ return registeredCount;
287
+ }
288
+ function buildToolsRegistry(readOnly, orgMode) {
289
+ const toolsMap = /* @__PURE__ */ new Map();
290
+ for (const tool of api.endpoints) {
291
+ const endpointConfig = endpointsData.find((e) => e.toolName === tool.alias);
292
+ if (!orgMode && endpointConfig && !endpointConfig.scopes && endpointConfig.workScopes) {
293
+ continue;
294
+ }
295
+ if (readOnly && tool.method.toUpperCase() !== "GET") {
296
+ continue;
297
+ }
298
+ toolsMap.set(tool.alias, { tool, config: endpointConfig });
299
+ }
300
+ return toolsMap;
301
+ }
302
+ function registerDiscoveryTools(server, graphClient, readOnly = false, orgMode = false) {
303
+ const toolsRegistry = buildToolsRegistry(readOnly, orgMode);
304
+ logger.info(`Discovery mode: ${toolsRegistry.size} tools available in registry`);
305
+ server.tool(
306
+ "search-tools",
307
+ `Search through ${toolsRegistry.size} available Microsoft Graph API tools. Use this to find tools by name, path, or description before executing them.`,
308
+ {
309
+ query: z.string().describe("Search query to filter tools (searches name, path, and description)").optional(),
310
+ category: z.string().describe(
311
+ "Filter by category: mail, calendar, files, contacts, tasks, onenote, search, users, excel"
312
+ ).optional(),
313
+ limit: z.number().describe("Maximum results to return (default: 20, max: 50)").optional()
314
+ },
315
+ {
316
+ title: "search-tools",
317
+ readOnlyHint: true,
318
+ openWorldHint: true
319
+ // Searches Microsoft Graph API tools
320
+ },
321
+ async ({ query, category, limit = 20 }) => {
322
+ const maxLimit = Math.min(limit, 50);
323
+ const results = [];
324
+ const queryLower = query?.toLowerCase();
325
+ const categoryDef = category ? TOOL_CATEGORIES[category] : void 0;
326
+ for (const [name, { tool, config }] of toolsRegistry) {
327
+ if (categoryDef && !categoryDef.pattern.test(name)) {
328
+ continue;
329
+ }
330
+ if (queryLower) {
331
+ const searchText = `${name} ${tool.path} ${tool.description || ""} ${config?.llmTip || ""}`.toLowerCase();
332
+ if (!searchText.includes(queryLower)) {
333
+ continue;
334
+ }
335
+ }
336
+ results.push({
337
+ name,
338
+ method: tool.method.toUpperCase(),
339
+ path: tool.path,
340
+ description: tool.description || `${tool.method.toUpperCase()} ${tool.path}`
341
+ });
342
+ if (results.length >= maxLimit) break;
343
+ }
344
+ return {
345
+ content: [
346
+ {
347
+ type: "text",
348
+ text: JSON.stringify(
349
+ {
350
+ found: results.length,
351
+ total: toolsRegistry.size,
352
+ tools: results,
353
+ tip: "Use execute-tool with the tool name and required parameters to call any of these tools."
354
+ },
355
+ null,
356
+ 2
357
+ )
358
+ }
359
+ ]
360
+ };
361
+ }
362
+ );
363
+ server.tool(
364
+ "execute-tool",
365
+ "Execute a Microsoft Graph API tool by name. Use search-tools first to find available tools and their parameters.",
366
+ {
367
+ tool_name: z.string().describe('Name of the tool to execute (e.g., "list-mail-messages")'),
368
+ parameters: z.record(z.any()).describe("Parameters to pass to the tool as key-value pairs").optional()
369
+ },
370
+ {
371
+ title: "execute-tool",
372
+ readOnlyHint: false,
373
+ destructiveHint: true,
374
+ // Can execute any tool, including write operations
375
+ openWorldHint: true
376
+ // Executes against Microsoft Graph API
377
+ },
378
+ async ({ tool_name, parameters = {} }) => {
379
+ const toolData = toolsRegistry.get(tool_name);
380
+ if (!toolData) {
381
+ return {
382
+ content: [
383
+ {
384
+ type: "text",
385
+ text: JSON.stringify({
386
+ error: `Tool not found: ${tool_name}`,
387
+ tip: "Use search-tools to find available tools."
388
+ })
389
+ }
390
+ ],
391
+ isError: true
392
+ };
393
+ }
394
+ return executeGraphTool(toolData.tool, toolData.config, graphClient, parameters);
395
+ }
396
+ );
397
+ }
398
+ export {
399
+ registerDiscoveryTools,
400
+ registerGraphTools
401
+ };