@softeria/ms-365-mcp-server
Version:
A Model Context Protocol (MCP) server for interacting with Microsoft 365 and Office services through the Graph API
228 lines (227 loc) • 7.76 kB
JavaScript
import logger from "./logger.js";
import { encode as toonEncode } from "@toon-format/toon";
import { getCloudEndpoints } from "./cloud-config.js";
import { getRequestTokens } from "./request-context.js";
import {
fetchWithResilience,
getSharedBreaker,
loadResilienceConfig
} from "./lib/graph-resilience.js";
function isBinaryContentType(contentType) {
if (!contentType) return false;
const lower = contentType.toLowerCase().split(";")[0].trim();
if (!lower) return false;
if (lower.startsWith("image/") || lower.startsWith("video/") || lower.startsWith("audio/") || lower.startsWith("font/")) {
return true;
}
if (lower === "application/octet-stream" || lower === "application/pdf") {
return true;
}
if (lower.startsWith("application/zip") || lower.startsWith("application/x-zip")) {
return true;
}
if (lower.startsWith("application/vnd.") || lower.startsWith("application/x-")) {
if (lower.endsWith("+json") || lower.endsWith("+xml") || lower.endsWith("+text")) {
return false;
}
return true;
}
return false;
}
class GraphClient {
constructor(authManager, secrets, outputFormat = "json") {
this.outputFormat = "json";
this.authManager = authManager;
this.secrets = secrets;
this.outputFormat = outputFormat;
}
async makeRequest(endpoint, options = {}) {
const contextTokens = getRequestTokens();
const accessToken = options.accessToken ?? contextTokens?.accessToken ?? await this.authManager.getToken();
if (!accessToken) {
throw new Error("No access token available");
}
try {
const response = await this.performRequest(endpoint, accessToken, options);
if (response.status === 403) {
const errorText = await response.text();
if (errorText.includes("scope") || errorText.includes("permission")) {
throw new Error(
`Microsoft Graph API scope error: ${response.status} ${response.statusText} - ${errorText}. This tool requires organization mode. Please restart with --org-mode flag.`
);
}
throw new Error(
`Microsoft Graph API error: ${response.status} ${response.statusText} - ${errorText}`
);
}
if (!response.ok) {
throw new Error(
`Microsoft Graph API error: ${response.status} ${response.statusText} - ${await response.text()}`
);
}
const contentTypeHeader = response.headers?.get?.("content-type") || "";
const isBinaryResponse = isBinaryContentType(contentTypeHeader);
let result;
if (isBinaryResponse) {
const buffer = Buffer.from(await response.arrayBuffer());
result = {
message: "OK!",
contentType: contentTypeHeader,
encoding: "base64",
contentLength: buffer.byteLength,
contentBytes: buffer.toString("base64")
};
} else {
const text = await response.text();
if (text === "") {
result = { message: "OK!" };
} else {
try {
result = JSON.parse(text);
} catch {
result = { message: "OK!", rawResponse: text };
}
}
}
if (options.includeHeaders) {
const etag = response.headers.get("ETag") || response.headers.get("etag");
if (result && typeof result === "object" && !Array.isArray(result)) {
return {
...result,
_etag: etag || "no-etag-found"
};
}
}
return result;
} catch (error) {
logger.error("Microsoft Graph API request failed:", error);
throw error;
}
}
async performRequest(endpoint, accessToken, options) {
const cloudEndpoints = getCloudEndpoints(this.secrets.cloudType);
const url = `${cloudEndpoints.graphApi}/v1.0${endpoint}`;
logger.info(`[GRAPH CLIENT] Final URL being sent to Microsoft: ${url}`);
const headers = {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json",
...options.headers
};
return fetchWithResilience(
url,
{
method: options.method || "GET",
headers,
// Node's fetch accepts Buffer/Uint8Array; TS BodyInit doesn't.
body: options.body
},
loadResilienceConfig(),
getSharedBreaker()
);
}
serializeData(data, outputFormat, pretty = false) {
if (outputFormat === "toon") {
try {
return toonEncode(data);
} catch (error) {
logger.warn(`Failed to encode as TOON, falling back to JSON: ${error}`);
return JSON.stringify(data, null, pretty ? 2 : void 0);
}
}
return JSON.stringify(data, null, pretty ? 2 : void 0);
}
async graphRequest(endpoint, options = {}) {
try {
logger.info(`Calling ${endpoint} with options: ${JSON.stringify(options)}`);
const result = await this.makeRequest(endpoint, options);
return this.formatJsonResponse(result, options.rawResponse, options.excludeResponse);
} catch (error) {
logger.error(`Error in Graph API request: ${error}`);
return {
content: [{ type: "text", text: JSON.stringify({ error: error.message }) }],
isError: true
};
}
}
formatJsonResponse(data, rawResponse = false, excludeResponse = false) {
if (excludeResponse) {
return {
content: [{ type: "text", text: this.serializeData({ success: true }, this.outputFormat) }]
};
}
if (data && typeof data === "object" && "_headers" in data) {
const responseData = data;
const meta = {};
if (responseData._etag) {
meta.etag = responseData._etag;
}
if (responseData._headers) {
meta.headers = responseData._headers;
}
if (rawResponse) {
return {
content: [
{ type: "text", text: this.serializeData(responseData.data, this.outputFormat) }
],
_meta: meta
};
}
if (responseData.data === null || responseData.data === void 0) {
return {
content: [
{ type: "text", text: this.serializeData({ success: true }, this.outputFormat) }
],
_meta: meta
};
}
const removeODataProps2 = (obj) => {
if (typeof obj === "object" && obj !== null) {
Object.keys(obj).forEach((key) => {
if (key.startsWith("@odata.") && key !== "@odata.nextLink") {
delete obj[key];
} else if (typeof obj[key] === "object") {
removeODataProps2(obj[key]);
}
});
}
};
removeODataProps2(responseData.data);
return {
content: [
{ type: "text", text: this.serializeData(responseData.data, this.outputFormat, true) }
],
_meta: meta
};
}
if (rawResponse) {
return {
content: [{ type: "text", text: this.serializeData(data, this.outputFormat) }]
};
}
if (data === null || data === void 0) {
return {
content: [{ type: "text", text: this.serializeData({ success: true }, this.outputFormat) }]
};
}
const removeODataProps = (obj) => {
if (typeof obj === "object" && obj !== null) {
Object.keys(obj).forEach((key) => {
if (key.startsWith("@odata.") && key !== "@odata.nextLink") {
delete obj[key];
} else if (typeof obj[key] === "object") {
removeODataProps(obj[key]);
}
});
}
};
removeODataProps(data);
return {
content: [{ type: "text", text: this.serializeData(data, this.outputFormat, true) }]
};
}
}
var graph_client_default = GraphClient;
export {
graph_client_default as default,
isBinaryContentType
};