@softeria/ms-365-mcp-server 0.132.1 → 0.133.1
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/dist/__tests__/graph-tools.test.js +156 -0
- package/dist/graph-client.js +61 -0
- package/dist/graph-tools.js +140 -2
- package/dist/mcp-instructions.js +1 -1
- package/dist/server.js +4 -2
- package/dist/tool-categories.js +1 -1
- package/package.json +1 -1
|
@@ -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 { mkdtempSync, readFileSync, rmSync, writeFileSync } from "fs";
|
|
3
6
|
vi.mock("../logger.js", () => ({
|
|
4
7
|
default: {
|
|
5
8
|
info: vi.fn(),
|
|
@@ -744,6 +747,159 @@ 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("streams bytes to the output path and returns metadata", async () => {
|
|
759
|
+
mockEndpoints.length = 0;
|
|
760
|
+
mockEndpointsJson = [];
|
|
761
|
+
const graphClient = {
|
|
762
|
+
downloadToFile: vi.fn().mockResolvedValue({
|
|
763
|
+
contentType: "image/jpeg",
|
|
764
|
+
contentLength: 2
|
|
765
|
+
})
|
|
766
|
+
};
|
|
767
|
+
const server = createMockServer();
|
|
768
|
+
const { registerGraphTools } = await loadModule();
|
|
769
|
+
registerGraphTools(server, graphClient);
|
|
770
|
+
const tool = server.tools.get("download-bytes-to-file");
|
|
771
|
+
expect(tool).toBeDefined();
|
|
772
|
+
const outputPath = join(tmpDir, "photo.jpg");
|
|
773
|
+
const result = await tool.handler({ target: "/me/photo/$value", outputPath });
|
|
774
|
+
expect(graphClient.downloadToFile).toHaveBeenCalledTimes(1);
|
|
775
|
+
const [reqPath, dest, options] = graphClient.downloadToFile.mock.calls[0];
|
|
776
|
+
expect(reqPath).toBe("/me/photo/$value");
|
|
777
|
+
expect(dest).toBe(outputPath);
|
|
778
|
+
expect(options).toStrictEqual({ accessToken: void 0 });
|
|
779
|
+
expect(result.isError).toBeUndefined();
|
|
780
|
+
const payload = JSON.parse(result.content[0].text);
|
|
781
|
+
expect(payload).toEqual({ path: outputPath, contentType: "image/jpeg", bytesWritten: 2 });
|
|
782
|
+
});
|
|
783
|
+
it("rejects a relative outputPath", async () => {
|
|
784
|
+
mockEndpoints.length = 0;
|
|
785
|
+
mockEndpointsJson = [];
|
|
786
|
+
const graphClient = { downloadToFile: vi.fn() };
|
|
787
|
+
const server = createMockServer();
|
|
788
|
+
const { registerGraphTools } = await loadModule();
|
|
789
|
+
registerGraphTools(server, graphClient);
|
|
790
|
+
const result = await server.tools.get("download-bytes-to-file").handler({ target: "/me/photo/$value", outputPath: "relative/path.jpg" });
|
|
791
|
+
expect(result.isError).toBe(true);
|
|
792
|
+
const payload = JSON.parse(result.content[0].text);
|
|
793
|
+
expect(payload.error).toMatch(/absolute path/);
|
|
794
|
+
expect(graphClient.downloadToFile).not.toHaveBeenCalled();
|
|
795
|
+
});
|
|
796
|
+
it("rejects absolute URLs in target (Graph paths only)", async () => {
|
|
797
|
+
mockEndpoints.length = 0;
|
|
798
|
+
mockEndpointsJson = [];
|
|
799
|
+
const server = createMockServer();
|
|
800
|
+
const { registerGraphTools } = await loadModule();
|
|
801
|
+
registerGraphTools(server, {});
|
|
802
|
+
const result = await server.tools.get("download-bytes-to-file").handler({
|
|
803
|
+
target: "https://example.sharepoint.com/d/abc?temp=signed",
|
|
804
|
+
outputPath: join(tmpDir, "x.bin")
|
|
805
|
+
});
|
|
806
|
+
expect(result.isError).toBe(true);
|
|
807
|
+
const payload = JSON.parse(result.content[0].text);
|
|
808
|
+
expect(payload.error).toMatch(/relative Microsoft Graph path/);
|
|
809
|
+
});
|
|
810
|
+
it("refuses to overwrite an existing file and skips the Graph call", async () => {
|
|
811
|
+
mockEndpoints.length = 0;
|
|
812
|
+
mockEndpointsJson = [];
|
|
813
|
+
const outputPath = join(tmpDir, "existing.bin");
|
|
814
|
+
writeFileSync(outputPath, "original");
|
|
815
|
+
const graphClient = { downloadToFile: vi.fn() };
|
|
816
|
+
const server = createMockServer();
|
|
817
|
+
const { registerGraphTools } = await loadModule();
|
|
818
|
+
registerGraphTools(server, graphClient);
|
|
819
|
+
const result = await server.tools.get("download-bytes-to-file").handler({ target: "/me/photo/$value", outputPath });
|
|
820
|
+
expect(result.isError).toBe(true);
|
|
821
|
+
const payload = JSON.parse(result.content[0].text);
|
|
822
|
+
expect(payload.error).toMatch(/already exists/);
|
|
823
|
+
expect(graphClient.downloadToFile).not.toHaveBeenCalled();
|
|
824
|
+
expect(readFileSync(outputPath).toString("utf8")).toBe("original");
|
|
825
|
+
});
|
|
826
|
+
it("surfaces a Graph error when downloadToFile throws", async () => {
|
|
827
|
+
mockEndpoints.length = 0;
|
|
828
|
+
mockEndpointsJson = [];
|
|
829
|
+
const graphClient = {
|
|
830
|
+
downloadToFile: vi.fn().mockRejectedValue(new Error("Microsoft Graph API error: 404 Not Found"))
|
|
831
|
+
};
|
|
832
|
+
const server = createMockServer();
|
|
833
|
+
const { registerGraphTools } = await loadModule();
|
|
834
|
+
registerGraphTools(server, graphClient);
|
|
835
|
+
const outputPath = join(tmpDir, "missing.bin");
|
|
836
|
+
const result = await server.tools.get("download-bytes-to-file").handler({ target: "/me/messages/abc/attachments/xyz/$value", outputPath });
|
|
837
|
+
expect(result.isError).toBe(true);
|
|
838
|
+
const payload = JSON.parse(result.content[0].text);
|
|
839
|
+
expect(payload.error).toMatch(/404 Not Found/);
|
|
840
|
+
});
|
|
841
|
+
it("forwards the resolved account token to downloadToFile in multi-account mode", async () => {
|
|
842
|
+
mockEndpoints.length = 0;
|
|
843
|
+
mockEndpointsJson = [];
|
|
844
|
+
const graphClient = {
|
|
845
|
+
downloadToFile: vi.fn().mockResolvedValue({ contentType: "application/pdf", contentLength: 3 })
|
|
846
|
+
};
|
|
847
|
+
const authManager = {
|
|
848
|
+
isOAuthModeEnabled: vi.fn().mockReturnValue(false),
|
|
849
|
+
getToken: vi.fn().mockResolvedValue(null),
|
|
850
|
+
getTokenForAccount: vi.fn().mockResolvedValue("account-2-token")
|
|
851
|
+
};
|
|
852
|
+
const server = createMockServer();
|
|
853
|
+
const { registerGraphTools } = await loadModule();
|
|
854
|
+
registerGraphTools(
|
|
855
|
+
server,
|
|
856
|
+
graphClient,
|
|
857
|
+
false,
|
|
858
|
+
void 0,
|
|
859
|
+
false,
|
|
860
|
+
authManager,
|
|
861
|
+
true,
|
|
862
|
+
["user1@domain.com", "user2@domain.com"]
|
|
863
|
+
);
|
|
864
|
+
const outputPath = join(tmpDir, "invoice.pdf");
|
|
865
|
+
const result = await server.tools.get("download-bytes-to-file").handler({
|
|
866
|
+
target: "/me/messages/m1/attachments/a1/$value",
|
|
867
|
+
outputPath,
|
|
868
|
+
account: "user2@domain.com"
|
|
869
|
+
});
|
|
870
|
+
expect(result.isError).toBeUndefined();
|
|
871
|
+
expect(authManager.getTokenForAccount).toHaveBeenCalledWith("user2@domain.com");
|
|
872
|
+
expect(graphClient.downloadToFile).toHaveBeenCalledWith(
|
|
873
|
+
"/me/messages/m1/attachments/a1/$value",
|
|
874
|
+
outputPath,
|
|
875
|
+
{ accessToken: "account-2-token" }
|
|
876
|
+
);
|
|
877
|
+
});
|
|
878
|
+
it("is registered in stdio mode but hidden in HTTP mode", async () => {
|
|
879
|
+
mockEndpoints.length = 0;
|
|
880
|
+
mockEndpointsJson = [];
|
|
881
|
+
const { registerGraphTools } = await loadModule();
|
|
882
|
+
const stdioServer = createMockServer();
|
|
883
|
+
registerGraphTools(stdioServer, {});
|
|
884
|
+
expect(stdioServer.tools.has("download-bytes-to-file")).toBe(true);
|
|
885
|
+
expect(stdioServer.tools.has("download-bytes")).toBe(true);
|
|
886
|
+
const httpServer = createMockServer();
|
|
887
|
+
registerGraphTools(
|
|
888
|
+
httpServer,
|
|
889
|
+
{},
|
|
890
|
+
false,
|
|
891
|
+
void 0,
|
|
892
|
+
false,
|
|
893
|
+
void 0,
|
|
894
|
+
false,
|
|
895
|
+
[],
|
|
896
|
+
void 0,
|
|
897
|
+
true
|
|
898
|
+
);
|
|
899
|
+
expect(httpServer.tools.has("download-bytes-to-file")).toBe(false);
|
|
900
|
+
expect(httpServer.tools.has("download-bytes")).toBe(true);
|
|
901
|
+
});
|
|
902
|
+
});
|
|
747
903
|
describe("get-download-url", () => {
|
|
748
904
|
it("strips /content, fetches item metadata, and returns the pre-authed downloadUrl", async () => {
|
|
749
905
|
mockEndpoints.length = 0;
|
package/dist/graph-client.js
CHANGED
|
@@ -7,6 +7,8 @@ import {
|
|
|
7
7
|
getSharedBreaker,
|
|
8
8
|
loadResilienceConfig
|
|
9
9
|
} from "./lib/graph-resilience.js";
|
|
10
|
+
import { open, stat, unlink } from "fs/promises";
|
|
11
|
+
import { pipeline } from "stream/promises";
|
|
10
12
|
function isBinaryContentType(contentType) {
|
|
11
13
|
if (!contentType) return false;
|
|
12
14
|
const lower = contentType.toLowerCase().split(";")[0].trim();
|
|
@@ -100,6 +102,65 @@ class GraphClient {
|
|
|
100
102
|
throw error;
|
|
101
103
|
}
|
|
102
104
|
}
|
|
105
|
+
/**
|
|
106
|
+
* Stream Graph byte content straight to a file, without holding the whole
|
|
107
|
+
* payload in memory. download-bytes-to-file uses this for big mail attachments
|
|
108
|
+
* and meeting recordings, where makeRequest's base64 buffering would blow up
|
|
109
|
+
* memory or hit V8's max string length. Creates the file with wx + 0o600 (never
|
|
110
|
+
* overwrites) and removes a partial file if the transfer fails.
|
|
111
|
+
*/
|
|
112
|
+
async downloadToFile(endpoint, destinationPath, options = {}) {
|
|
113
|
+
const fileHandle = await open(destinationPath, "wx", 384);
|
|
114
|
+
let completed = false;
|
|
115
|
+
try {
|
|
116
|
+
const contextTokens = getRequestTokens();
|
|
117
|
+
const accessToken = options.accessToken ?? contextTokens?.accessToken ?? await this.authManager.getToken();
|
|
118
|
+
if (!accessToken) {
|
|
119
|
+
throw new Error("No access token available");
|
|
120
|
+
}
|
|
121
|
+
const response = await this.performRequest(endpoint, accessToken, options);
|
|
122
|
+
if (response.status === 403) {
|
|
123
|
+
const errorText = await response.text();
|
|
124
|
+
if (errorText.includes("scope") || errorText.includes("permission")) {
|
|
125
|
+
throw new Error(
|
|
126
|
+
`Microsoft Graph API scope error: ${response.status} ${response.statusText} - ${errorText}. This tool requires organization mode. Please restart with --org-mode flag.`
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
throw new Error(
|
|
130
|
+
`Microsoft Graph API error: ${response.status} ${response.statusText} - ${errorText}`
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
if (!response.ok) {
|
|
134
|
+
throw new Error(
|
|
135
|
+
`Microsoft Graph API error: ${response.status} ${response.statusText} - ${await response.text()}`
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
if (!response.body) {
|
|
139
|
+
throw new Error("Microsoft Graph returned an empty response body");
|
|
140
|
+
}
|
|
141
|
+
await pipeline(response.body, fileHandle.createWriteStream());
|
|
142
|
+
completed = true;
|
|
143
|
+
let contentLength;
|
|
144
|
+
try {
|
|
145
|
+
contentLength = (await stat(destinationPath)).size;
|
|
146
|
+
} catch {
|
|
147
|
+
const header = Number(response.headers.get("content-length"));
|
|
148
|
+
contentLength = Number.isFinite(header) ? header : 0;
|
|
149
|
+
}
|
|
150
|
+
return {
|
|
151
|
+
contentType: response.headers.get("content-type") || "application/octet-stream",
|
|
152
|
+
contentLength
|
|
153
|
+
};
|
|
154
|
+
} catch (error) {
|
|
155
|
+
logger.error("Microsoft Graph file download failed:", error);
|
|
156
|
+
throw error;
|
|
157
|
+
} finally {
|
|
158
|
+
await fileHandle.close().catch(() => void 0);
|
|
159
|
+
if (!completed) {
|
|
160
|
+
await unlink(destinationPath).catch(() => void 0);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
103
164
|
async performRequest(endpoint, accessToken, options) {
|
|
104
165
|
const cloudEndpoints = getCloudEndpoints(this.secrets.cloudType);
|
|
105
166
|
const apiVersion = options.apiVersion || "v1.0";
|
package/dist/graph-tools.js
CHANGED
|
@@ -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 { 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,141 @@ 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.downloadToFile(target, outputPath, {
|
|
303
|
+
accessToken: accountAccessToken
|
|
304
|
+
});
|
|
305
|
+
return {
|
|
306
|
+
content: [
|
|
307
|
+
{
|
|
308
|
+
type: "text",
|
|
309
|
+
text: JSON.stringify({
|
|
310
|
+
path: outputPath,
|
|
311
|
+
contentType: result.contentType,
|
|
312
|
+
bytesWritten: result.contentLength
|
|
313
|
+
})
|
|
314
|
+
}
|
|
315
|
+
]
|
|
316
|
+
};
|
|
317
|
+
} catch (error) {
|
|
318
|
+
return {
|
|
319
|
+
content: [{ type: "text", text: JSON.stringify({ error: error.message }) }],
|
|
320
|
+
isError: true
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
},
|
|
189
325
|
{
|
|
190
326
|
name: "get-download-url",
|
|
191
327
|
method: "GET",
|
|
@@ -810,7 +946,7 @@ async function executeGraphTool(tool, config, graphClient, params, authManager)
|
|
|
810
946
|
};
|
|
811
947
|
}
|
|
812
948
|
}
|
|
813
|
-
function registerGraphTools(server, graphClient, readOnly = false, enabledToolsPattern, orgMode = false, authManager, multiAccount = false, accountNames = [], allowedScopesValue) {
|
|
949
|
+
function registerGraphTools(server, graphClient, readOnly = false, enabledToolsPattern, orgMode = false, authManager, multiAccount = false, accountNames = [], allowedScopesValue, httpMode = false) {
|
|
814
950
|
let enabledToolsRegex;
|
|
815
951
|
if (enabledToolsPattern) {
|
|
816
952
|
try {
|
|
@@ -983,6 +1119,7 @@ function registerGraphTools(server, graphClient, readOnly = false, enabledToolsP
|
|
|
983
1119
|
};
|
|
984
1120
|
for (const utility of UTILITY_TOOLS) {
|
|
985
1121
|
if (readOnly && !utility.readOnlyHint) continue;
|
|
1122
|
+
if (httpMode && utility.stdioOnly) continue;
|
|
986
1123
|
if (enabledToolsRegex && !enabledToolsRegex.test(utility.name)) continue;
|
|
987
1124
|
try {
|
|
988
1125
|
registerUtilityToolWithMcp(server, utility, utilityCtx);
|
|
@@ -1098,7 +1235,7 @@ function scoreDiscoveryQuery(query, index) {
|
|
|
1098
1235
|
ranked.sort((a, b) => b.score - a.score);
|
|
1099
1236
|
return ranked;
|
|
1100
1237
|
}
|
|
1101
|
-
function registerDiscoveryTools(server, graphClient, readOnly = false, orgMode = false, authManager, multiAccount = false, accountNames = [], enabledTools, allowedScopesValue) {
|
|
1238
|
+
function registerDiscoveryTools(server, graphClient, readOnly = false, orgMode = false, authManager, multiAccount = false, accountNames = [], enabledTools, allowedScopesValue, httpMode = false) {
|
|
1102
1239
|
let enabledToolsRegex;
|
|
1103
1240
|
if (enabledTools) {
|
|
1104
1241
|
try {
|
|
@@ -1125,6 +1262,7 @@ function registerDiscoveryTools(server, graphClient, readOnly = false, orgMode =
|
|
|
1125
1262
|
}
|
|
1126
1263
|
const utilityTools = UTILITY_TOOLS.filter((u) => {
|
|
1127
1264
|
if (readOnly && !u.readOnlyHint) return false;
|
|
1265
|
+
if (httpMode && u.stdioOnly) return false;
|
|
1128
1266
|
if (enabledToolsRegex && !enabledToolsRegex.test(u.name)) return false;
|
|
1129
1267
|
return true;
|
|
1130
1268
|
});
|
package/dist/mcp-instructions.js
CHANGED
|
@@ -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.
|
|
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);
|
package/dist/tool-categories.js
CHANGED
|
@@ -53,7 +53,7 @@ const PRESET_META = {
|
|
|
53
53
|
requiresOrgMode: true
|
|
54
54
|
}
|
|
55
55
|
};
|
|
56
|
-
const UNIVERSAL_UTILITY_TOOLS = ["download-bytes"];
|
|
56
|
+
const UNIVERSAL_UTILITY_TOOLS = ["download-bytes", "download-bytes-to-file"];
|
|
57
57
|
const SCOPED_UTILITY_TOOLS = {
|
|
58
58
|
"get-download-url": ["files", "onedrive", "personal", "work", "search"],
|
|
59
59
|
"parse-teams-url": ["teams", "work"]
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@softeria/ms-365-mcp-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.133.1",
|
|
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",
|