@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.
- package/.env.example +54 -0
- package/.releaserc.json +9 -0
- package/Dockerfile +22 -0
- package/LICENSE +21 -0
- package/README.md +590 -0
- package/bin/generate-graph-client.mjs +59 -0
- package/bin/modules/download-openapi.mjs +40 -0
- package/bin/modules/extract-descriptions.mjs +48 -0
- package/bin/modules/generate-mcp-tools.mjs +46 -0
- package/bin/modules/simplified-openapi.mjs +694 -0
- package/dist/auth-tools.js +202 -0
- package/dist/auth.js +422 -0
- package/dist/cli.js +78 -0
- package/dist/cloud-config.js +49 -0
- package/dist/endpoints.json +596 -0
- package/dist/generated/endpoint-types.js +0 -0
- package/dist/generated/hack.js +42 -0
- package/dist/graph-client.js +208 -0
- package/dist/graph-tools.js +401 -0
- package/dist/index.js +76 -0
- package/dist/lib/microsoft-auth.js +73 -0
- package/dist/logger.js +42 -0
- package/dist/oauth-provider.js +51 -0
- package/dist/request-context.js +9 -0
- package/dist/secrets.js +68 -0
- package/dist/server.js +387 -0
- package/dist/tool-categories.js +93 -0
- package/dist/version.js +10 -0
- package/eslint.config.js +43 -0
- package/glama.json +4 -0
- package/package.json +79 -0
- package/remove-recursive-refs.js +294 -0
- package/src/endpoints.json +596 -0
- package/src/generated/README.md +56 -0
- package/src/generated/endpoint-types.ts +27 -0
- package/src/generated/hack.ts +49 -0
- package/test-calendar-fix.js +62 -0
- package/test-real-calendar.js +96 -0
- package/tsup.config.ts +30 -0
|
@@ -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
|
+
};
|