@softeria/ms-365-mcp-server 0.125.1 → 0.127.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/bin/modules/simplified-openapi.mjs +27 -0
- package/dist/__tests__/graph-tools.test.js +352 -0
- package/dist/endpoints.json +26 -3
- package/dist/generated/client-beta.js +141 -1
- package/dist/graph-tools.js +207 -3
- package/dist/mcp-instructions.js +1 -1
- package/package.json +1 -1
- package/src/endpoints.json +26 -3
|
@@ -145,6 +145,10 @@ export function createAndSaveSimplifiedOpenAPI(
|
|
|
145
145
|
}
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
+
if (apiVersion === 'beta') {
|
|
149
|
+
normalizeWildcardSuccessResponses(openApiSpec.paths);
|
|
150
|
+
}
|
|
151
|
+
|
|
148
152
|
if (openApiSpec.components && openApiSpec.components.schemas) {
|
|
149
153
|
removeODataTypeRecursively(openApiSpec.components.schemas);
|
|
150
154
|
flattenComplexSchemasRecursively(openApiSpec.components.schemas);
|
|
@@ -162,6 +166,29 @@ export function createAndSaveSimplifiedOpenAPI(
|
|
|
162
166
|
fs.writeFileSync(openapiTrimmedFile, yaml.dump(openApiSpec));
|
|
163
167
|
}
|
|
164
168
|
|
|
169
|
+
function normalizeWildcardSuccessResponses(paths) {
|
|
170
|
+
Object.values(paths || {}).forEach((pathItem) => {
|
|
171
|
+
if (!pathItem || typeof pathItem !== 'object') return;
|
|
172
|
+
|
|
173
|
+
Object.entries(pathItem).forEach(([method, operation]) => {
|
|
174
|
+
if (!operation || typeof operation !== 'object') return;
|
|
175
|
+
if (!operation.responses || !operation.responses['2XX']) return;
|
|
176
|
+
|
|
177
|
+
const hasConcreteSuccess = Object.keys(operation.responses).some((statusCode) =>
|
|
178
|
+
/^2\d\d$/.test(statusCode)
|
|
179
|
+
);
|
|
180
|
+
if (hasConcreteSuccess) {
|
|
181
|
+
delete operation.responses['2XX'];
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const successStatus = method === 'post' ? '201' : method === 'delete' ? '204' : '200';
|
|
186
|
+
operation.responses[successStatus] = operation.responses['2XX'];
|
|
187
|
+
delete operation.responses['2XX'];
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
165
192
|
function removeODataTypeRecursively(obj) {
|
|
166
193
|
if (!obj || typeof obj !== 'object') return;
|
|
167
194
|
|
|
@@ -91,6 +91,10 @@ function createMockServer() {
|
|
|
91
91
|
tools
|
|
92
92
|
};
|
|
93
93
|
}
|
|
94
|
+
function makeJwt(payload) {
|
|
95
|
+
const body = Buffer.from(JSON.stringify(payload)).toString("base64url");
|
|
96
|
+
return `header.${body}.signature`;
|
|
97
|
+
}
|
|
94
98
|
describe("graph-tools", () => {
|
|
95
99
|
beforeEach(() => {
|
|
96
100
|
mockEndpoints.length = 0;
|
|
@@ -665,6 +669,351 @@ describe("graph-tools", () => {
|
|
|
665
669
|
expect(payload.error).toMatch(/relative Microsoft Graph path/);
|
|
666
670
|
});
|
|
667
671
|
});
|
|
672
|
+
describe("get-download-url", () => {
|
|
673
|
+
it("strips /content, fetches item metadata, and returns the pre-authed downloadUrl", async () => {
|
|
674
|
+
mockEndpoints.length = 0;
|
|
675
|
+
mockEndpointsJson = [];
|
|
676
|
+
const downloadUrl = "https://contoso.sharepoint.com/download.aspx?tempauth=abc";
|
|
677
|
+
const graphClient = {
|
|
678
|
+
graphRequest: vi.fn().mockResolvedValue({
|
|
679
|
+
content: [
|
|
680
|
+
{
|
|
681
|
+
type: "text",
|
|
682
|
+
text: JSON.stringify({
|
|
683
|
+
id: "item1",
|
|
684
|
+
name: "report.pdf",
|
|
685
|
+
size: 12727,
|
|
686
|
+
file: { mimeType: "application/pdf" },
|
|
687
|
+
"@microsoft.graph.downloadUrl": downloadUrl
|
|
688
|
+
})
|
|
689
|
+
}
|
|
690
|
+
]
|
|
691
|
+
})
|
|
692
|
+
};
|
|
693
|
+
const server = createMockServer();
|
|
694
|
+
const { registerGraphTools } = await loadModule();
|
|
695
|
+
registerGraphTools(server, graphClient);
|
|
696
|
+
const tool = server.tools.get("get-download-url");
|
|
697
|
+
expect(tool).toBeDefined();
|
|
698
|
+
const result = await tool.handler({
|
|
699
|
+
target: "/drives/d1/items/item1/content"
|
|
700
|
+
});
|
|
701
|
+
const [requestedPath] = graphClient.graphRequest.mock.calls[0];
|
|
702
|
+
expect(requestedPath).toBe("/drives/d1/items/item1");
|
|
703
|
+
const payload = JSON.parse(result.content[0].text);
|
|
704
|
+
expect(payload.downloadUrl).toBe(downloadUrl);
|
|
705
|
+
expect(payload.name).toBe("report.pdf");
|
|
706
|
+
expect(payload.size).toBe(12727);
|
|
707
|
+
expect(payload.contentType).toBe("application/pdf");
|
|
708
|
+
});
|
|
709
|
+
it("rejects query-shaped targets instead of silently changing request semantics", async () => {
|
|
710
|
+
mockEndpoints.length = 0;
|
|
711
|
+
mockEndpointsJson = [];
|
|
712
|
+
const graphClient = { graphRequest: vi.fn() };
|
|
713
|
+
const server = createMockServer();
|
|
714
|
+
const { registerGraphTools } = await loadModule();
|
|
715
|
+
registerGraphTools(server, graphClient);
|
|
716
|
+
const tool = server.tools.get("get-download-url");
|
|
717
|
+
const result = await tool.handler({
|
|
718
|
+
target: "/drives/d1/items/item1/content?$select=id,name"
|
|
719
|
+
});
|
|
720
|
+
expect(result.isError).toBe(true);
|
|
721
|
+
expect(graphClient.graphRequest).not.toHaveBeenCalled();
|
|
722
|
+
const payload = JSON.parse(result.content[0].text);
|
|
723
|
+
expect(payload.error).toContain("must not include query parameters");
|
|
724
|
+
});
|
|
725
|
+
it("rejects non-drive Graph targets before making an authenticated request", async () => {
|
|
726
|
+
mockEndpoints.length = 0;
|
|
727
|
+
mockEndpointsJson = [];
|
|
728
|
+
const graphClient = { graphRequest: vi.fn() };
|
|
729
|
+
const server = createMockServer();
|
|
730
|
+
const { registerGraphTools } = await loadModule();
|
|
731
|
+
registerGraphTools(server, graphClient);
|
|
732
|
+
const tool = server.tools.get("get-download-url");
|
|
733
|
+
const result = await tool.handler({
|
|
734
|
+
target: "/me/messages/m1"
|
|
735
|
+
});
|
|
736
|
+
expect(result.isError).toBe(true);
|
|
737
|
+
expect(graphClient.graphRequest).not.toHaveBeenCalled();
|
|
738
|
+
const payload = JSON.parse(result.content[0].text);
|
|
739
|
+
expect(payload.error).toContain("target must identify a driveItem");
|
|
740
|
+
});
|
|
741
|
+
it("rejects mail attachment $value paths (no pre-authed URL exists)", async () => {
|
|
742
|
+
mockEndpoints.length = 0;
|
|
743
|
+
mockEndpointsJson = [];
|
|
744
|
+
const graphClient = { graphRequest: vi.fn() };
|
|
745
|
+
const server = createMockServer();
|
|
746
|
+
const { registerGraphTools } = await loadModule();
|
|
747
|
+
registerGraphTools(server, graphClient);
|
|
748
|
+
const tool = server.tools.get("get-download-url");
|
|
749
|
+
const result = await tool.handler({
|
|
750
|
+
target: "/me/messages/m1/attachments/a1/$value"
|
|
751
|
+
});
|
|
752
|
+
expect(result.isError).toBe(true);
|
|
753
|
+
expect(graphClient.graphRequest).not.toHaveBeenCalled();
|
|
754
|
+
const payload = JSON.parse(result.content[0].text);
|
|
755
|
+
expect(payload.error).toMatch(/do not expose a pre-authenticated download URL/);
|
|
756
|
+
});
|
|
757
|
+
it("rejects calendar event attachment $value paths (no pre-authed URL exists)", async () => {
|
|
758
|
+
mockEndpoints.length = 0;
|
|
759
|
+
mockEndpointsJson = [];
|
|
760
|
+
const graphClient = { graphRequest: vi.fn() };
|
|
761
|
+
const server = createMockServer();
|
|
762
|
+
const { registerGraphTools } = await loadModule();
|
|
763
|
+
registerGraphTools(server, graphClient);
|
|
764
|
+
const tool = server.tools.get("get-download-url");
|
|
765
|
+
const result = await tool.handler({
|
|
766
|
+
target: "/me/events/e1/attachments/a1/$value"
|
|
767
|
+
});
|
|
768
|
+
expect(result.isError).toBe(true);
|
|
769
|
+
expect(graphClient.graphRequest).not.toHaveBeenCalled();
|
|
770
|
+
const payload = JSON.parse(result.content[0].text);
|
|
771
|
+
expect(payload.error).toMatch(/Mail and calendar event attachments/);
|
|
772
|
+
});
|
|
773
|
+
it("rejects group mailbox attachment paths (no pre-authed URL exists)", async () => {
|
|
774
|
+
mockEndpoints.length = 0;
|
|
775
|
+
mockEndpointsJson = [];
|
|
776
|
+
const graphClient = { graphRequest: vi.fn() };
|
|
777
|
+
const server = createMockServer();
|
|
778
|
+
const { registerGraphTools } = await loadModule();
|
|
779
|
+
registerGraphTools(server, graphClient);
|
|
780
|
+
const tool = server.tools.get("get-download-url");
|
|
781
|
+
const result = await tool.handler({
|
|
782
|
+
target: "/groups/g1/messages/m1/attachments/a1/$value"
|
|
783
|
+
});
|
|
784
|
+
expect(result.isError).toBe(true);
|
|
785
|
+
expect(graphClient.graphRequest).not.toHaveBeenCalled();
|
|
786
|
+
const payload = JSON.parse(result.content[0].text);
|
|
787
|
+
expect(payload.error).toMatch(/Mail and calendar event attachments/);
|
|
788
|
+
});
|
|
789
|
+
it("rejects list-item driveItem relationships until callers provide a drive item path", async () => {
|
|
790
|
+
mockEndpoints.length = 0;
|
|
791
|
+
mockEndpointsJson = [];
|
|
792
|
+
const graphClient = { graphRequest: vi.fn() };
|
|
793
|
+
const server = createMockServer();
|
|
794
|
+
const { registerGraphTools } = await loadModule();
|
|
795
|
+
registerGraphTools(server, graphClient);
|
|
796
|
+
const tool = server.tools.get("get-download-url");
|
|
797
|
+
const result = await tool.handler({
|
|
798
|
+
target: "/sites/site1/lists/list1/items/item1/driveItem"
|
|
799
|
+
});
|
|
800
|
+
expect(result.isError).toBe(true);
|
|
801
|
+
expect(graphClient.graphRequest).not.toHaveBeenCalled();
|
|
802
|
+
const payload = JSON.parse(result.content[0].text);
|
|
803
|
+
expect(payload.error).toContain("target must identify a driveItem");
|
|
804
|
+
});
|
|
805
|
+
it("errors when the resource exposes no downloadUrl", async () => {
|
|
806
|
+
mockEndpoints.length = 0;
|
|
807
|
+
mockEndpointsJson = [];
|
|
808
|
+
const graphClient = {
|
|
809
|
+
graphRequest: vi.fn().mockResolvedValue({
|
|
810
|
+
content: [{ type: "text", text: JSON.stringify({ id: "item1", name: "x" }) }]
|
|
811
|
+
})
|
|
812
|
+
};
|
|
813
|
+
const server = createMockServer();
|
|
814
|
+
const { registerGraphTools } = await loadModule();
|
|
815
|
+
registerGraphTools(server, graphClient);
|
|
816
|
+
const tool = server.tools.get("get-download-url");
|
|
817
|
+
const result = await tool.handler({ target: "/drives/d1/items/item1" });
|
|
818
|
+
expect(result.isError).toBe(true);
|
|
819
|
+
const payload = JSON.parse(result.content[0].text);
|
|
820
|
+
expect(payload.error).toMatch(/No pre-authenticated download URL/);
|
|
821
|
+
});
|
|
822
|
+
it("surfaces the underlying Graph error instead of masking it as no-downloadUrl", async () => {
|
|
823
|
+
mockEndpoints.length = 0;
|
|
824
|
+
mockEndpointsJson = [];
|
|
825
|
+
const graphClient = {
|
|
826
|
+
graphRequest: vi.fn().mockResolvedValue({
|
|
827
|
+
isError: true,
|
|
828
|
+
content: [
|
|
829
|
+
{
|
|
830
|
+
type: "text",
|
|
831
|
+
text: JSON.stringify({ error: "Microsoft Graph API error: 403 Forbidden" })
|
|
832
|
+
}
|
|
833
|
+
]
|
|
834
|
+
})
|
|
835
|
+
};
|
|
836
|
+
const server = createMockServer();
|
|
837
|
+
const { registerGraphTools } = await loadModule();
|
|
838
|
+
registerGraphTools(server, graphClient);
|
|
839
|
+
const tool = server.tools.get("get-download-url");
|
|
840
|
+
const result = await tool.handler({ target: "/drives/d1/items/item1/content" });
|
|
841
|
+
expect(result.isError).toBe(true);
|
|
842
|
+
const payload = JSON.parse(result.content[0].text);
|
|
843
|
+
expect(payload.error).toMatch(/403 Forbidden/);
|
|
844
|
+
expect(payload.error).not.toMatch(/No pre-authenticated download URL/);
|
|
845
|
+
});
|
|
846
|
+
it('does not falsely reject drive folders literally named "attachments"', async () => {
|
|
847
|
+
mockEndpoints.length = 0;
|
|
848
|
+
mockEndpointsJson = [];
|
|
849
|
+
const downloadUrl = "https://contoso.sharepoint.com/download.aspx?tempauth=xyz";
|
|
850
|
+
const graphClient = {
|
|
851
|
+
graphRequest: vi.fn().mockResolvedValue({
|
|
852
|
+
content: [
|
|
853
|
+
{
|
|
854
|
+
type: "text",
|
|
855
|
+
text: JSON.stringify({
|
|
856
|
+
name: "report.pdf",
|
|
857
|
+
"@microsoft.graph.downloadUrl": downloadUrl
|
|
858
|
+
})
|
|
859
|
+
}
|
|
860
|
+
]
|
|
861
|
+
})
|
|
862
|
+
};
|
|
863
|
+
const server = createMockServer();
|
|
864
|
+
const { registerGraphTools } = await loadModule();
|
|
865
|
+
registerGraphTools(server, graphClient);
|
|
866
|
+
const tool = server.tools.get("get-download-url");
|
|
867
|
+
const result = await tool.handler({
|
|
868
|
+
target: "/me/drive/root:/Project/attachments/report.pdf:/content"
|
|
869
|
+
});
|
|
870
|
+
expect(result.isError).toBeFalsy();
|
|
871
|
+
const payload = JSON.parse(result.content[0].text);
|
|
872
|
+
expect(payload.downloadUrl).toBe(downloadUrl);
|
|
873
|
+
});
|
|
874
|
+
it("does not falsely reject drive item paths containing messages and attachments folders", async () => {
|
|
875
|
+
mockEndpoints.length = 0;
|
|
876
|
+
mockEndpointsJson = [];
|
|
877
|
+
const downloadUrl = "https://contoso.sharepoint.com/download.aspx?tempauth=folders";
|
|
878
|
+
const graphClient = {
|
|
879
|
+
graphRequest: vi.fn().mockResolvedValue({
|
|
880
|
+
content: [
|
|
881
|
+
{
|
|
882
|
+
type: "text",
|
|
883
|
+
text: JSON.stringify({
|
|
884
|
+
name: "report.pdf",
|
|
885
|
+
"@microsoft.graph.downloadUrl": downloadUrl
|
|
886
|
+
})
|
|
887
|
+
}
|
|
888
|
+
]
|
|
889
|
+
})
|
|
890
|
+
};
|
|
891
|
+
const server = createMockServer();
|
|
892
|
+
const { registerGraphTools } = await loadModule();
|
|
893
|
+
registerGraphTools(server, graphClient);
|
|
894
|
+
const tool = server.tools.get("get-download-url");
|
|
895
|
+
const result = await tool.handler({
|
|
896
|
+
target: "/me/drive/root:/messages/m1/attachments/a1/report.pdf:/content"
|
|
897
|
+
});
|
|
898
|
+
expect(result.isError).toBeFalsy();
|
|
899
|
+
const [requestedPath] = graphClient.graphRequest.mock.calls[0];
|
|
900
|
+
expect(requestedPath).toBe("/me/drive/root:/messages/m1/attachments/a1/report.pdf:");
|
|
901
|
+
const payload = JSON.parse(result.content[0].text);
|
|
902
|
+
expect(payload.downloadUrl).toBe(downloadUrl);
|
|
903
|
+
});
|
|
904
|
+
it("allows SharePoint site drive item paths", async () => {
|
|
905
|
+
mockEndpoints.length = 0;
|
|
906
|
+
mockEndpointsJson = [];
|
|
907
|
+
const downloadUrl = "https://contoso.sharepoint.com/download.aspx?tempauth=site-drive";
|
|
908
|
+
const graphClient = {
|
|
909
|
+
graphRequest: vi.fn().mockResolvedValue({
|
|
910
|
+
content: [
|
|
911
|
+
{
|
|
912
|
+
type: "text",
|
|
913
|
+
text: JSON.stringify({
|
|
914
|
+
name: "site-report.pdf",
|
|
915
|
+
"@microsoft.graph.downloadUrl": downloadUrl
|
|
916
|
+
})
|
|
917
|
+
}
|
|
918
|
+
]
|
|
919
|
+
})
|
|
920
|
+
};
|
|
921
|
+
const server = createMockServer();
|
|
922
|
+
const { registerGraphTools } = await loadModule();
|
|
923
|
+
registerGraphTools(server, graphClient);
|
|
924
|
+
const tool = server.tools.get("get-download-url");
|
|
925
|
+
const result = await tool.handler({
|
|
926
|
+
target: "/sites/site1/drive/items/item1/content"
|
|
927
|
+
});
|
|
928
|
+
expect(result.isError).toBeFalsy();
|
|
929
|
+
const [requestedPath] = graphClient.graphRequest.mock.calls[0];
|
|
930
|
+
expect(requestedPath).toBe("/sites/site1/drive/items/item1");
|
|
931
|
+
const payload = JSON.parse(result.content[0].text);
|
|
932
|
+
expect(payload.downloadUrl).toBe(downloadUrl);
|
|
933
|
+
});
|
|
934
|
+
it("does not strip a drive item path whose item name is content", async () => {
|
|
935
|
+
mockEndpoints.length = 0;
|
|
936
|
+
mockEndpointsJson = [];
|
|
937
|
+
const downloadUrl = "https://contoso.sharepoint.com/download.aspx?tempauth=content-file";
|
|
938
|
+
const graphClient = {
|
|
939
|
+
graphRequest: vi.fn().mockResolvedValue({
|
|
940
|
+
content: [
|
|
941
|
+
{
|
|
942
|
+
type: "text",
|
|
943
|
+
text: JSON.stringify({
|
|
944
|
+
name: "content",
|
|
945
|
+
"@microsoft.graph.downloadUrl": downloadUrl
|
|
946
|
+
})
|
|
947
|
+
}
|
|
948
|
+
]
|
|
949
|
+
})
|
|
950
|
+
};
|
|
951
|
+
const server = createMockServer();
|
|
952
|
+
const { registerGraphTools } = await loadModule();
|
|
953
|
+
registerGraphTools(server, graphClient);
|
|
954
|
+
const tool = server.tools.get("get-download-url");
|
|
955
|
+
const result = await tool.handler({
|
|
956
|
+
target: "/me/drive/root:/Project/content:"
|
|
957
|
+
});
|
|
958
|
+
expect(result.isError).toBeFalsy();
|
|
959
|
+
const [requestedPath] = graphClient.graphRequest.mock.calls[0];
|
|
960
|
+
expect(requestedPath).toBe("/me/drive/root:/Project/content:");
|
|
961
|
+
const payload = JSON.parse(result.content[0].text);
|
|
962
|
+
expect(payload.downloadUrl).toBe(downloadUrl);
|
|
963
|
+
});
|
|
964
|
+
it("rejects meeting recording content paths because Graph returns authenticated bytes", async () => {
|
|
965
|
+
mockEndpoints.length = 0;
|
|
966
|
+
mockEndpointsJson = [];
|
|
967
|
+
const graphClient = { graphRequest: vi.fn() };
|
|
968
|
+
const server = createMockServer();
|
|
969
|
+
const { registerGraphTools } = await loadModule();
|
|
970
|
+
registerGraphTools(server, graphClient);
|
|
971
|
+
const tool = server.tools.get("get-download-url");
|
|
972
|
+
const result = await tool.handler({
|
|
973
|
+
target: "/me/onlineMeetings/meeting1/recordings/recording1/content"
|
|
974
|
+
});
|
|
975
|
+
expect(result.isError).toBe(true);
|
|
976
|
+
expect(graphClient.graphRequest).not.toHaveBeenCalled();
|
|
977
|
+
const payload = JSON.parse(result.content[0].text);
|
|
978
|
+
expect(payload.error).toMatch(/Meeting recordings do not expose/);
|
|
979
|
+
});
|
|
980
|
+
it("refuses mismatched account param in bearer mode before resolving download URL", async () => {
|
|
981
|
+
mockEndpoints.length = 0;
|
|
982
|
+
mockEndpointsJson = [];
|
|
983
|
+
const graphClient = { graphRequest: vi.fn() };
|
|
984
|
+
const authManager = {
|
|
985
|
+
isOAuthModeEnabled: vi.fn().mockReturnValue(false),
|
|
986
|
+
getToken: vi.fn().mockResolvedValue(null),
|
|
987
|
+
getTokenForAccount: vi.fn()
|
|
988
|
+
};
|
|
989
|
+
const server = createMockServer();
|
|
990
|
+
const { registerGraphTools } = await loadModule();
|
|
991
|
+
registerGraphTools(
|
|
992
|
+
server,
|
|
993
|
+
graphClient,
|
|
994
|
+
false,
|
|
995
|
+
void 0,
|
|
996
|
+
false,
|
|
997
|
+
authManager,
|
|
998
|
+
true,
|
|
999
|
+
["user1@domain.com", "user2@domain.com"]
|
|
1000
|
+
);
|
|
1001
|
+
const { requestContext } = await import("../request-context.js");
|
|
1002
|
+
const tool = server.tools.get("get-download-url");
|
|
1003
|
+
const bearer = makeJwt({ upn: "user1@domain.com" });
|
|
1004
|
+
const result = await requestContext.run(
|
|
1005
|
+
{ accessToken: bearer },
|
|
1006
|
+
() => tool.handler({
|
|
1007
|
+
target: "/drives/d1/items/item1/content",
|
|
1008
|
+
account: "user2@domain.com"
|
|
1009
|
+
})
|
|
1010
|
+
);
|
|
1011
|
+
expect(result.isError).toBe(true);
|
|
1012
|
+
expect(result.content[0].text).toContain("'account' parameter is not supported");
|
|
1013
|
+
expect(graphClient.graphRequest).not.toHaveBeenCalled();
|
|
1014
|
+
expect(authManager.getTokenForAccount).not.toHaveBeenCalled();
|
|
1015
|
+
});
|
|
1016
|
+
});
|
|
668
1017
|
describe("allowed scopes filtering", () => {
|
|
669
1018
|
it("registerGraphTools hides Graph tools outside the allowed scopes", async () => {
|
|
670
1019
|
mockEndpoints.push(
|
|
@@ -788,6 +1137,9 @@ describe("graph-tools", () => {
|
|
|
788
1137
|
const targetParam = schema.parameters.find((p) => p.name === "target");
|
|
789
1138
|
expect(targetParam).toBeDefined();
|
|
790
1139
|
expect(targetParam.required).toBe(true);
|
|
1140
|
+
expect(targetParam.description).toContain("authenticated recording bytes");
|
|
1141
|
+
expect(targetParam.description).not.toContain("returns a URL");
|
|
1142
|
+
expect(schema.description).toContain("For large drive/SharePoint file content");
|
|
791
1143
|
});
|
|
792
1144
|
it("execute-tool dispatches to download-bytes for a Graph path", async () => {
|
|
793
1145
|
mockEndpoints.length = 0;
|
package/dist/endpoints.json
CHANGED
|
@@ -594,7 +594,7 @@
|
|
|
594
594
|
"toolName": "get-drive-item",
|
|
595
595
|
"presets": ["files", "onedrive", "personal"],
|
|
596
596
|
"scopes": ["Files.Read"],
|
|
597
|
-
"llmTip": "Gets metadata for a file or folder: name, size, lastModifiedDateTime, createdBy, webUrl, file (mimeType, hashes), folder (childCount), parentReference, and @microsoft.graph.downloadUrl. For
|
|
597
|
+
"llmTip": "Gets metadata for a file or folder: name, size, lastModifiedDateTime, createdBy, webUrl, file (mimeType, hashes), folder (childCount), parentReference, and @microsoft.graph.downloadUrl. For large drive/SharePoint files, call get-download-url with target=/drives/{drive-id}/items/{driveItem-id}/content to fetch out-of-band with no Authorization header. For small files where base64 in the tool response is acceptable, call download-bytes with the same /content target."
|
|
598
598
|
},
|
|
599
599
|
{
|
|
600
600
|
"pathPattern": "/drives/{drive-id}/items/{driveItem-id}",
|
|
@@ -1167,6 +1167,30 @@
|
|
|
1167
1167
|
"scopes": ["Tasks.ReadWrite"],
|
|
1168
1168
|
"llmTip": "CRITICAL: Requires If-Match header with ETag from get-planner-bucket (use includeHeaders=true)."
|
|
1169
1169
|
},
|
|
1170
|
+
{
|
|
1171
|
+
"pathPattern": "/planner/tasks/{plannerTask-id}/messages",
|
|
1172
|
+
"method": "get",
|
|
1173
|
+
"toolName": "list-planner-task-messages",
|
|
1174
|
+
"scopes": ["Tasks.Read"],
|
|
1175
|
+
"apiVersion": "beta",
|
|
1176
|
+
"llmTip": "Lists messages in a Planner task's chat — the modern Planner 'task chat', distinct from the legacy conversationThreadId comments (which live in the M365 group conversation thread). Each message has id, content (HTML), createdBy, createdDateTime, mentions, reactions. BETA Graph API: subject to change; delegated work/school accounts only — no application permissions, no personal Microsoft accounts, global cloud only (not GCC/DoD/21Vianet)."
|
|
1177
|
+
},
|
|
1178
|
+
{
|
|
1179
|
+
"pathPattern": "/planner/tasks/{plannerTask-id}/messages",
|
|
1180
|
+
"method": "post",
|
|
1181
|
+
"toolName": "create-planner-task-message",
|
|
1182
|
+
"scopes": ["Tasks.ReadWrite"],
|
|
1183
|
+
"apiVersion": "beta",
|
|
1184
|
+
"llmTip": "Posts a message to a Planner task's chat (the modern 'task chat', not the legacy conversationThreadId comment). Microsoft is retiring the classic task comments experience and hiding it from the task (Planner 2026 update), so task chat is the current supported way to post per-task updates teammates will see, with @mentions. Body: { content: 'plain text or sanitized HTML', mentions?: [{ mentioned: 'user-id', position: 0, mentionType: 'user' }] } (mentions optional). No ETag/If-Match required. BETA Graph API: subject to change; delegated work/school accounts only — no application permissions, no personal Microsoft accounts, global cloud only (not GCC/DoD/21Vianet)."
|
|
1185
|
+
},
|
|
1186
|
+
{
|
|
1187
|
+
"pathPattern": "/planner/tasks/{plannerTask-id}/messages/{plannerTaskChatMessage-id}",
|
|
1188
|
+
"method": "delete",
|
|
1189
|
+
"toolName": "delete-planner-task-message",
|
|
1190
|
+
"scopes": ["Tasks.ReadWrite"],
|
|
1191
|
+
"apiVersion": "beta",
|
|
1192
|
+
"llmTip": "Deletes a message from a Planner task's chat. No request body; If-Match is optional if you want conditional deletion; returns 204. BETA Graph API: subject to change; delegated work/school accounts only — no application permissions, no personal Microsoft accounts, global cloud only (not GCC/DoD/21Vianet)."
|
|
1193
|
+
},
|
|
1170
1194
|
{
|
|
1171
1195
|
"pathPattern": "/me/contacts",
|
|
1172
1196
|
"method": "get",
|
|
@@ -1880,8 +1904,7 @@
|
|
|
1880
1904
|
"toolName": "get-meeting-recording-content",
|
|
1881
1905
|
"presets": ["teams", "work"],
|
|
1882
1906
|
"workScopes": ["OnlineMeetingRecording.Read.All"],
|
|
1883
|
-
"
|
|
1884
|
-
"llmTip": "Returns a temporary download URL for the meeting recording in MP4 format. Use the download URL to access the video file. The recording may be large — do not attempt to stream it inline."
|
|
1907
|
+
"llmTip": "Returns the authenticated meeting recording video bytes in MP4 format. The recording may be large; prefer out-of-band handling by the client when possible, but Microsoft Graph does not expose a pre-authenticated download URL for this resource."
|
|
1885
1908
|
},
|
|
1886
1909
|
{
|
|
1887
1910
|
"pathPattern": "/me/onlineMeetings/{onlineMeeting-id}/attendanceReports",
|
|
@@ -690,6 +690,58 @@ const microsoft_graph_ODataErrors_MainError = z.object({
|
|
|
690
690
|
innerError: microsoft_graph_ODataErrors_InnerError.optional()
|
|
691
691
|
}).passthrough();
|
|
692
692
|
const microsoft_graph_ODataErrors_ODataError = z.object({ error: microsoft_graph_ODataErrors_MainError }).passthrough();
|
|
693
|
+
const microsoft_graph_plannerTaskChatMentionType = z.enum([
|
|
694
|
+
"user",
|
|
695
|
+
"application",
|
|
696
|
+
"unknownFutureValue"
|
|
697
|
+
]);
|
|
698
|
+
const microsoft_graph_plannerTaskChatMention = z.object({
|
|
699
|
+
mentioned: z.string().describe("The ID of the mentioned user.").nullish(),
|
|
700
|
+
mentionType: microsoft_graph_plannerTaskChatMentionType.optional(),
|
|
701
|
+
position: z.number().gte(-2147483648).lte(2147483647).describe("The zero-based position of the mention in the message content.").optional()
|
|
702
|
+
}).passthrough();
|
|
703
|
+
const microsoft_graph_plannerTaskChatMessageType = z.enum([
|
|
704
|
+
"richTextHtml",
|
|
705
|
+
"plainText",
|
|
706
|
+
"unknownFutureValue"
|
|
707
|
+
]);
|
|
708
|
+
const microsoft_graph_plannerTaskChatReactionEvent = z.object({
|
|
709
|
+
createdBy: microsoft_graph_identitySet.optional(),
|
|
710
|
+
createdDateTime: z.string().regex(
|
|
711
|
+
/^[0-9]{4,}-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])T([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9]([.][0-9]{1,12})?(Z|[+-][0-9][0-9]:[0-9][0-9])$/
|
|
712
|
+
).datetime({ offset: true }).describe(
|
|
713
|
+
"The date and time when the reaction was added. The timestamp type represents date and time information using ISO 8601 format and is always in UTC. For example, midnight UTC on Jan 1, 2014 is 2014-01-01T00:00:00Z."
|
|
714
|
+
).optional()
|
|
715
|
+
}).passthrough();
|
|
716
|
+
const microsoft_graph_plannerTaskChatReaction = z.object({
|
|
717
|
+
reactionEvents: z.array(microsoft_graph_plannerTaskChatReactionEvent).optional(),
|
|
718
|
+
reactionType: z.string().describe("The type of reaction, such as like, heart, or emoji characters.").nullish()
|
|
719
|
+
}).passthrough();
|
|
720
|
+
const microsoft_graph_plannerTaskChatMessage = z.object({
|
|
721
|
+
id: z.string().describe("The unique identifier for an entity. Read-only.").optional(),
|
|
722
|
+
content: z.string().describe("The content of the chat message. Supports plain text and sanitized HTML.").nullish(),
|
|
723
|
+
createdBy: microsoft_graph_identitySet.optional(),
|
|
724
|
+
createdDateTime: z.string().regex(
|
|
725
|
+
/^[0-9]{4,}-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])T([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9]([.][0-9]{1,12})?(Z|[+-][0-9][0-9]:[0-9][0-9])$/
|
|
726
|
+
).datetime({ offset: true }).describe(
|
|
727
|
+
"The date and time when the message was created. The timestamp type represents date and time information using ISO 8601 format and is always in UTC. For example, midnight UTC on Jan 1, 2014 is 2014-01-01T00:00:00Z."
|
|
728
|
+
).optional(),
|
|
729
|
+
deletedDateTime: z.string().regex(
|
|
730
|
+
/^[0-9]{4,}-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])T([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9]([.][0-9]{1,12})?(Z|[+-][0-9][0-9]:[0-9][0-9])$/
|
|
731
|
+
).datetime({ offset: true }).nullish(),
|
|
732
|
+
editedDateTime: z.string().regex(
|
|
733
|
+
/^[0-9]{4,}-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])T([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9]([.][0-9]{1,12})?(Z|[+-][0-9][0-9]:[0-9][0-9])$/
|
|
734
|
+
).datetime({ offset: true }).nullish(),
|
|
735
|
+
mentions: z.array(microsoft_graph_plannerTaskChatMention).describe("The list of mentions in the message.").optional(),
|
|
736
|
+
messageType: microsoft_graph_plannerTaskChatMessageType.optional(),
|
|
737
|
+
parentEntityId: z.string().describe("The ID of the parent plannerTask that this message belongs to.").nullish(),
|
|
738
|
+
reactions: z.array(microsoft_graph_plannerTaskChatReaction).describe("The reactions on the message.").optional()
|
|
739
|
+
}).passthrough();
|
|
740
|
+
const microsoft_graph_plannerTaskChatMessageCollectionResponse = z.object({
|
|
741
|
+
"@odata.count": z.number().int().nullable(),
|
|
742
|
+
"@odata.nextLink": z.string().nullable(),
|
|
743
|
+
value: z.array(microsoft_graph_plannerTaskChatMessage)
|
|
744
|
+
}).partial().passthrough();
|
|
693
745
|
const schemas = {
|
|
694
746
|
microsoft_graph_allowedAudiences,
|
|
695
747
|
microsoft_graph_identity,
|
|
@@ -741,7 +793,14 @@ const schemas = {
|
|
|
741
793
|
microsoft_graph_ODataErrors_ErrorDetails,
|
|
742
794
|
microsoft_graph_ODataErrors_InnerError,
|
|
743
795
|
microsoft_graph_ODataErrors_MainError,
|
|
744
|
-
microsoft_graph_ODataErrors_ODataError
|
|
796
|
+
microsoft_graph_ODataErrors_ODataError,
|
|
797
|
+
microsoft_graph_plannerTaskChatMentionType,
|
|
798
|
+
microsoft_graph_plannerTaskChatMention,
|
|
799
|
+
microsoft_graph_plannerTaskChatMessageType,
|
|
800
|
+
microsoft_graph_plannerTaskChatReactionEvent,
|
|
801
|
+
microsoft_graph_plannerTaskChatReaction,
|
|
802
|
+
microsoft_graph_plannerTaskChatMessage,
|
|
803
|
+
microsoft_graph_plannerTaskChatMessageCollectionResponse
|
|
745
804
|
};
|
|
746
805
|
const endpoints = makeApi([
|
|
747
806
|
{
|
|
@@ -762,6 +821,87 @@ const endpoints = makeApi([
|
|
|
762
821
|
schema: z.array(z.string()).describe("Expand related entities").optional()
|
|
763
822
|
}
|
|
764
823
|
],
|
|
824
|
+
response: microsoft_graph_profile
|
|
825
|
+
},
|
|
826
|
+
{
|
|
827
|
+
method: "get",
|
|
828
|
+
path: "/planner/tasks/:plannerTaskId/messages",
|
|
829
|
+
alias: "list-planner-task-messages",
|
|
830
|
+
description: `Retrieve a list of plannerTaskChatMessage objects associated with a plannerTask.`,
|
|
831
|
+
requestFormat: "json",
|
|
832
|
+
parameters: [
|
|
833
|
+
{
|
|
834
|
+
name: "$top",
|
|
835
|
+
type: "Query",
|
|
836
|
+
schema: z.number().int().gte(0).describe("Show only the first n items").optional()
|
|
837
|
+
},
|
|
838
|
+
{
|
|
839
|
+
name: "$skip",
|
|
840
|
+
type: "Query",
|
|
841
|
+
schema: z.number().int().gte(0).describe("Skip the first n items").optional()
|
|
842
|
+
},
|
|
843
|
+
{
|
|
844
|
+
name: "$search",
|
|
845
|
+
type: "Query",
|
|
846
|
+
schema: z.string().describe("Search items by search phrases").optional()
|
|
847
|
+
},
|
|
848
|
+
{
|
|
849
|
+
name: "$filter",
|
|
850
|
+
type: "Query",
|
|
851
|
+
schema: z.string().describe("Filter items by property values").optional()
|
|
852
|
+
},
|
|
853
|
+
{
|
|
854
|
+
name: "$count",
|
|
855
|
+
type: "Query",
|
|
856
|
+
schema: z.boolean().describe("Include count of items").optional()
|
|
857
|
+
},
|
|
858
|
+
{
|
|
859
|
+
name: "$orderby",
|
|
860
|
+
type: "Query",
|
|
861
|
+
schema: z.array(z.string()).describe("Order items by property values").optional()
|
|
862
|
+
},
|
|
863
|
+
{
|
|
864
|
+
name: "$select",
|
|
865
|
+
type: "Query",
|
|
866
|
+
schema: z.array(z.string()).describe("Select properties to be returned").optional()
|
|
867
|
+
},
|
|
868
|
+
{
|
|
869
|
+
name: "$expand",
|
|
870
|
+
type: "Query",
|
|
871
|
+
schema: z.array(z.string()).describe("Expand related entities").optional()
|
|
872
|
+
}
|
|
873
|
+
],
|
|
874
|
+
response: microsoft_graph_plannerTaskChatMessageCollectionResponse
|
|
875
|
+
},
|
|
876
|
+
{
|
|
877
|
+
method: "post",
|
|
878
|
+
path: "/planner/tasks/:plannerTaskId/messages",
|
|
879
|
+
alias: "create-planner-task-message",
|
|
880
|
+
description: `Create a new plannerTaskChatMessage on a plannerTask.`,
|
|
881
|
+
requestFormat: "json",
|
|
882
|
+
parameters: [
|
|
883
|
+
{
|
|
884
|
+
name: "body",
|
|
885
|
+
description: `New navigation property`,
|
|
886
|
+
type: "Body",
|
|
887
|
+
schema: microsoft_graph_plannerTaskChatMessage
|
|
888
|
+
}
|
|
889
|
+
],
|
|
890
|
+
response: microsoft_graph_plannerTaskChatMessage
|
|
891
|
+
},
|
|
892
|
+
{
|
|
893
|
+
method: "delete",
|
|
894
|
+
path: "/planner/tasks/:plannerTaskId/messages/:plannerTaskChatMessageId",
|
|
895
|
+
alias: "delete-planner-task-message",
|
|
896
|
+
description: `Delete a plannerTaskChatMessage object.`,
|
|
897
|
+
requestFormat: "json",
|
|
898
|
+
parameters: [
|
|
899
|
+
{
|
|
900
|
+
name: "If-Match",
|
|
901
|
+
type: "Header",
|
|
902
|
+
schema: z.string().describe("ETag").optional()
|
|
903
|
+
}
|
|
904
|
+
],
|
|
765
905
|
response: z.void()
|
|
766
906
|
}
|
|
767
907
|
]);
|
package/dist/graph-tools.js
CHANGED
|
@@ -115,13 +115,13 @@ const UTILITY_TOOLS = [
|
|
|
115
115
|
name: "download-bytes",
|
|
116
116
|
method: "GET",
|
|
117
117
|
path: "tool:download-bytes",
|
|
118
|
-
description: 'Download binary content from Microsoft Graph and return it as base64. Single tool for any binary read: drive file content, mail attachment, profile photo, Teams hosted content, meeting recording. Returns { contentType, encoding: "base64", contentLength, contentBytes }.',
|
|
118
|
+
description: 'Download binary content from Microsoft Graph and return it as base64. Single tool for any binary read: drive file content, mail attachment, profile photo, Teams hosted content, meeting recording. Returns { contentType, encoding: "base64", contentLength, contentBytes }. For large drive/SharePoint file content, prefer get-download-url, which returns a pre-authenticated URL to stream bytes out-of-band instead of base64 through the agent context.',
|
|
119
119
|
readOnlyHint: true,
|
|
120
120
|
openWorldHint: true,
|
|
121
121
|
buildSchema: (ctx) => {
|
|
122
122
|
const schema = {
|
|
123
123
|
target: z.string().describe(
|
|
124
|
-
'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
|
|
124
|
+
'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.'
|
|
125
125
|
)
|
|
126
126
|
};
|
|
127
127
|
if (ctx.multiAccount) {
|
|
@@ -178,6 +178,198 @@ const UTILITY_TOOLS = [
|
|
|
178
178
|
};
|
|
179
179
|
}
|
|
180
180
|
}
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
name: "get-download-url",
|
|
184
|
+
method: "GET",
|
|
185
|
+
path: "tool:get-download-url",
|
|
186
|
+
searchKeywords: "download file download drive file download onedrive file sharepoint file download large drive file large sharepoint file large file out-of-band download pre-authenticated url",
|
|
187
|
+
description: "Resolve a short-lived, pre-authenticated download URL for Microsoft Graph binary content that exposes one (drive/SharePoint file content). The returned URL streams the bytes with NO Authorization header, so the client can fetch it straight to disk (e.g. curl) without round-tripping base64 through the agent context. Prefer this over download-bytes for any file above a few KB or any bulk download. Returns { downloadUrl, name?, size?, contentType? }. NOTE: mail file attachments (/messages/{id}/attachments/{id}/$value) and meeting recordings do NOT expose a pre-authenticated URL \u2014 Graph offers no such link for them; use download-bytes for small ones.",
|
|
188
|
+
readOnlyHint: true,
|
|
189
|
+
openWorldHint: true,
|
|
190
|
+
buildSchema: (ctx) => {
|
|
191
|
+
const schema = {
|
|
192
|
+
target: z.string().describe(
|
|
193
|
+
'Relative Microsoft Graph path starting with "/". Either a driveItem content path or the item path itself, e.g. /drives/{drive-id}/items/{driveItem-id}/content, /me/drive/items/{driveItem-id}/content, or /sites/{site-id}/drive/items/{driveItem-id}. A trailing /content is optional and is stripped automatically for drive items. Mail attachment $value paths and meeting recordings are not supported (Graph exposes no pre-authenticated URL for them).'
|
|
194
|
+
)
|
|
195
|
+
};
|
|
196
|
+
if (ctx.multiAccount) {
|
|
197
|
+
schema["account"] = z.string().optional().describe(
|
|
198
|
+
"Account to use when multiple Microsoft accounts are configured. Required when multiple accounts exist (see list-accounts)."
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
return schema;
|
|
202
|
+
},
|
|
203
|
+
execute: async (params, { graphClient, authManager }) => {
|
|
204
|
+
const target = params.target;
|
|
205
|
+
const accountParam = params.account;
|
|
206
|
+
if (typeof target !== "string" || target.length === 0) {
|
|
207
|
+
return {
|
|
208
|
+
content: [
|
|
209
|
+
{
|
|
210
|
+
type: "text",
|
|
211
|
+
text: JSON.stringify({ error: "target is required and must be a non-empty string." })
|
|
212
|
+
}
|
|
213
|
+
],
|
|
214
|
+
isError: true
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
if (!target.startsWith("/")) {
|
|
218
|
+
return {
|
|
219
|
+
content: [
|
|
220
|
+
{
|
|
221
|
+
type: "text",
|
|
222
|
+
text: JSON.stringify({
|
|
223
|
+
error: 'target must be a relative Microsoft Graph path starting with "/", e.g. /drives/{drive-id}/items/{driveItem-id}/content.'
|
|
224
|
+
})
|
|
225
|
+
}
|
|
226
|
+
],
|
|
227
|
+
isError: true
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
const queryIdx = target.indexOf("?");
|
|
231
|
+
if (queryIdx >= 0) {
|
|
232
|
+
return {
|
|
233
|
+
content: [
|
|
234
|
+
{
|
|
235
|
+
type: "text",
|
|
236
|
+
text: JSON.stringify({
|
|
237
|
+
error: "target must not include query parameters. Pass the drive item /content path or item metadata path without $select, $expand, or other query options."
|
|
238
|
+
})
|
|
239
|
+
}
|
|
240
|
+
],
|
|
241
|
+
isError: true
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
const pathPart = target.replace(/\/+$/, "");
|
|
245
|
+
if (/^(\/me|\/users\/[^/]+)\/messages\/[^/]+\/attachments\//.test(pathPart) || /^(\/me|\/users\/[^/]+)\/events\/[^/]+\/attachments\//.test(pathPart) || /^\/groups\/[^/]+\/messages\/[^/]+\/attachments\//.test(pathPart) || /^\/groups\/[^/]+\/events\/[^/]+\/attachments\//.test(pathPart)) {
|
|
246
|
+
return {
|
|
247
|
+
content: [
|
|
248
|
+
{
|
|
249
|
+
type: "text",
|
|
250
|
+
text: JSON.stringify({
|
|
251
|
+
error: "Mail and calendar event attachments do not expose a pre-authenticated download URL. Use download-bytes for small attachments."
|
|
252
|
+
})
|
|
253
|
+
}
|
|
254
|
+
],
|
|
255
|
+
isError: true
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
if (/^(\/me|\/users\/[^/]+)\/onlineMeetings\/[^/]+\/recordings\/[^/]+(?:\/content)?$/.test(
|
|
259
|
+
pathPart
|
|
260
|
+
) || /^\/communications\/calls\/[^/]+\/recordings\/[^/]+(?:\/content)?$/.test(pathPart)) {
|
|
261
|
+
return {
|
|
262
|
+
content: [
|
|
263
|
+
{
|
|
264
|
+
type: "text",
|
|
265
|
+
text: JSON.stringify({
|
|
266
|
+
error: "Meeting recordings do not expose a pre-authenticated download URL. Use download-bytes for small recordings or get-meeting-recording-content where available."
|
|
267
|
+
})
|
|
268
|
+
}
|
|
269
|
+
],
|
|
270
|
+
isError: true
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
if (pathPart.endsWith("/$value")) {
|
|
274
|
+
return {
|
|
275
|
+
content: [
|
|
276
|
+
{
|
|
277
|
+
type: "text",
|
|
278
|
+
text: JSON.stringify({
|
|
279
|
+
error: "$value byte endpoints do not expose a pre-authenticated download URL. Use download-bytes to read these bytes."
|
|
280
|
+
})
|
|
281
|
+
}
|
|
282
|
+
],
|
|
283
|
+
isError: true
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
const isDriveItemById = /^\/drives\/[^/]+\/items\/[^/]+(?:\/content)?$/.test(pathPart) || /^\/(?:me|users\/[^/]+|groups\/[^/]+|sites\/[^/]+)\/drive\/items\/[^/]+(?:\/content)?$/.test(
|
|
287
|
+
pathPart
|
|
288
|
+
) || /^\/(?:groups\/[^/]+|sites\/[^/]+)\/drives\/[^/]+\/items\/[^/]+(?:\/content)?$/.test(
|
|
289
|
+
pathPart
|
|
290
|
+
);
|
|
291
|
+
const isDriveItemByPath = /^\/drives\/[^/]+\/root:\/.+:(?:\/content)?$/.test(pathPart) || /^\/(?:me|users\/[^/]+|groups\/[^/]+|sites\/[^/]+)\/drive\/root:\/.+:(?:\/content)?$/.test(
|
|
292
|
+
pathPart
|
|
293
|
+
) || /^\/(?:groups\/[^/]+|sites\/[^/]+)\/drives\/[^/]+\/root:\/.+:(?:\/content)?$/.test(
|
|
294
|
+
pathPart
|
|
295
|
+
);
|
|
296
|
+
if (!isDriveItemById && !isDriveItemByPath) {
|
|
297
|
+
return {
|
|
298
|
+
content: [
|
|
299
|
+
{
|
|
300
|
+
type: "text",
|
|
301
|
+
text: JSON.stringify({
|
|
302
|
+
error: "target must identify a driveItem in OneDrive or SharePoint. Use a drive item metadata path or /content path, such as /drives/{drive-id}/items/{driveItem-id}/content, /me/drive/items/{driveItem-id}, /sites/{site-id}/drive/items/{driveItem-id}, or /me/drive/root:/path/file.ext:/content. Other Graph byte resources must use download-bytes."
|
|
303
|
+
})
|
|
304
|
+
}
|
|
305
|
+
],
|
|
306
|
+
isError: true
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
const isDriveContentEndpoint = /\/items\/[^/]+\/content$/.test(pathPart) || pathPart.endsWith(":/content");
|
|
310
|
+
const itemPath = isDriveContentEndpoint ? pathPart.slice(0, -"/content".length) : pathPart;
|
|
311
|
+
try {
|
|
312
|
+
const accountModeError = await checkAccountParamInBearerMode(accountParam, authManager);
|
|
313
|
+
if (accountModeError) {
|
|
314
|
+
return {
|
|
315
|
+
content: [{ type: "text", text: JSON.stringify({ error: accountModeError }) }],
|
|
316
|
+
isError: true
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
let accountAccessToken;
|
|
320
|
+
if (authManager && !authManager.isOAuthModeEnabled() && !getRequestTokens()) {
|
|
321
|
+
accountAccessToken = await authManager.getTokenForAccount(accountParam);
|
|
322
|
+
}
|
|
323
|
+
const response = await graphClient.graphRequest(itemPath, {
|
|
324
|
+
accessToken: accountAccessToken
|
|
325
|
+
});
|
|
326
|
+
if (response?.isError) {
|
|
327
|
+
return response;
|
|
328
|
+
}
|
|
329
|
+
const text = response?.content?.[0]?.text;
|
|
330
|
+
let item;
|
|
331
|
+
if (typeof text === "string") {
|
|
332
|
+
try {
|
|
333
|
+
item = JSON.parse(text);
|
|
334
|
+
} catch {
|
|
335
|
+
item = void 0;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
const downloadUrl = item?.["@microsoft.graph.downloadUrl"];
|
|
339
|
+
if (!downloadUrl) {
|
|
340
|
+
return {
|
|
341
|
+
content: [
|
|
342
|
+
{
|
|
343
|
+
type: "text",
|
|
344
|
+
text: JSON.stringify({
|
|
345
|
+
error: "No pre-authenticated download URL is available for this resource. It may not be a drive item, or it exposes bytes only via download-bytes."
|
|
346
|
+
})
|
|
347
|
+
}
|
|
348
|
+
],
|
|
349
|
+
isError: true
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
const file = item?.file;
|
|
353
|
+
return {
|
|
354
|
+
content: [
|
|
355
|
+
{
|
|
356
|
+
type: "text",
|
|
357
|
+
text: JSON.stringify({
|
|
358
|
+
downloadUrl,
|
|
359
|
+
name: item?.name,
|
|
360
|
+
size: item?.size,
|
|
361
|
+
contentType: file?.mimeType
|
|
362
|
+
})
|
|
363
|
+
}
|
|
364
|
+
]
|
|
365
|
+
};
|
|
366
|
+
} catch (error) {
|
|
367
|
+
return {
|
|
368
|
+
content: [{ type: "text", text: JSON.stringify({ error: error.message }) }],
|
|
369
|
+
isError: true
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
}
|
|
181
373
|
}
|
|
182
374
|
];
|
|
183
375
|
function registerUtilityToolWithMcp(server, utility, ctx) {
|
|
@@ -747,8 +939,20 @@ function buildDiscoverySearchIndex(toolsRegistry, utilityTools = []) {
|
|
|
747
939
|
const nt = tokenize(utility.name);
|
|
748
940
|
nameTokens.set(utility.name, new Set(nt));
|
|
749
941
|
const pathTokens = tokenize(utility.path);
|
|
942
|
+
const keywordTokens = tokenize(utility.searchKeywords);
|
|
750
943
|
const descTokens = tokenize(utility.description).slice(0, DESC_CAP_TOKENS);
|
|
751
|
-
const tokens = [
|
|
944
|
+
const tokens = [
|
|
945
|
+
...nt,
|
|
946
|
+
...nt,
|
|
947
|
+
...nt,
|
|
948
|
+
...nt,
|
|
949
|
+
...nt,
|
|
950
|
+
...pathTokens,
|
|
951
|
+
...pathTokens,
|
|
952
|
+
...keywordTokens,
|
|
953
|
+
...keywordTokens,
|
|
954
|
+
...descTokens
|
|
955
|
+
];
|
|
752
956
|
docs.push({ id: utility.name, tokens });
|
|
753
957
|
}
|
|
754
958
|
return { bm25: buildBM25Index(docs), nameTokens };
|
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:
|
|
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. Both 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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@softeria/ms-365-mcp-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.127.0",
|
|
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",
|
package/src/endpoints.json
CHANGED
|
@@ -594,7 +594,7 @@
|
|
|
594
594
|
"toolName": "get-drive-item",
|
|
595
595
|
"presets": ["files", "onedrive", "personal"],
|
|
596
596
|
"scopes": ["Files.Read"],
|
|
597
|
-
"llmTip": "Gets metadata for a file or folder: name, size, lastModifiedDateTime, createdBy, webUrl, file (mimeType, hashes), folder (childCount), parentReference, and @microsoft.graph.downloadUrl. For
|
|
597
|
+
"llmTip": "Gets metadata for a file or folder: name, size, lastModifiedDateTime, createdBy, webUrl, file (mimeType, hashes), folder (childCount), parentReference, and @microsoft.graph.downloadUrl. For large drive/SharePoint files, call get-download-url with target=/drives/{drive-id}/items/{driveItem-id}/content to fetch out-of-band with no Authorization header. For small files where base64 in the tool response is acceptable, call download-bytes with the same /content target."
|
|
598
598
|
},
|
|
599
599
|
{
|
|
600
600
|
"pathPattern": "/drives/{drive-id}/items/{driveItem-id}",
|
|
@@ -1167,6 +1167,30 @@
|
|
|
1167
1167
|
"scopes": ["Tasks.ReadWrite"],
|
|
1168
1168
|
"llmTip": "CRITICAL: Requires If-Match header with ETag from get-planner-bucket (use includeHeaders=true)."
|
|
1169
1169
|
},
|
|
1170
|
+
{
|
|
1171
|
+
"pathPattern": "/planner/tasks/{plannerTask-id}/messages",
|
|
1172
|
+
"method": "get",
|
|
1173
|
+
"toolName": "list-planner-task-messages",
|
|
1174
|
+
"scopes": ["Tasks.Read"],
|
|
1175
|
+
"apiVersion": "beta",
|
|
1176
|
+
"llmTip": "Lists messages in a Planner task's chat — the modern Planner 'task chat', distinct from the legacy conversationThreadId comments (which live in the M365 group conversation thread). Each message has id, content (HTML), createdBy, createdDateTime, mentions, reactions. BETA Graph API: subject to change; delegated work/school accounts only — no application permissions, no personal Microsoft accounts, global cloud only (not GCC/DoD/21Vianet)."
|
|
1177
|
+
},
|
|
1178
|
+
{
|
|
1179
|
+
"pathPattern": "/planner/tasks/{plannerTask-id}/messages",
|
|
1180
|
+
"method": "post",
|
|
1181
|
+
"toolName": "create-planner-task-message",
|
|
1182
|
+
"scopes": ["Tasks.ReadWrite"],
|
|
1183
|
+
"apiVersion": "beta",
|
|
1184
|
+
"llmTip": "Posts a message to a Planner task's chat (the modern 'task chat', not the legacy conversationThreadId comment). Microsoft is retiring the classic task comments experience and hiding it from the task (Planner 2026 update), so task chat is the current supported way to post per-task updates teammates will see, with @mentions. Body: { content: 'plain text or sanitized HTML', mentions?: [{ mentioned: 'user-id', position: 0, mentionType: 'user' }] } (mentions optional). No ETag/If-Match required. BETA Graph API: subject to change; delegated work/school accounts only — no application permissions, no personal Microsoft accounts, global cloud only (not GCC/DoD/21Vianet)."
|
|
1185
|
+
},
|
|
1186
|
+
{
|
|
1187
|
+
"pathPattern": "/planner/tasks/{plannerTask-id}/messages/{plannerTaskChatMessage-id}",
|
|
1188
|
+
"method": "delete",
|
|
1189
|
+
"toolName": "delete-planner-task-message",
|
|
1190
|
+
"scopes": ["Tasks.ReadWrite"],
|
|
1191
|
+
"apiVersion": "beta",
|
|
1192
|
+
"llmTip": "Deletes a message from a Planner task's chat. No request body; If-Match is optional if you want conditional deletion; returns 204. BETA Graph API: subject to change; delegated work/school accounts only — no application permissions, no personal Microsoft accounts, global cloud only (not GCC/DoD/21Vianet)."
|
|
1193
|
+
},
|
|
1170
1194
|
{
|
|
1171
1195
|
"pathPattern": "/me/contacts",
|
|
1172
1196
|
"method": "get",
|
|
@@ -1880,8 +1904,7 @@
|
|
|
1880
1904
|
"toolName": "get-meeting-recording-content",
|
|
1881
1905
|
"presets": ["teams", "work"],
|
|
1882
1906
|
"workScopes": ["OnlineMeetingRecording.Read.All"],
|
|
1883
|
-
"
|
|
1884
|
-
"llmTip": "Returns a temporary download URL for the meeting recording in MP4 format. Use the download URL to access the video file. The recording may be large — do not attempt to stream it inline."
|
|
1907
|
+
"llmTip": "Returns the authenticated meeting recording video bytes in MP4 format. The recording may be large; prefer out-of-band handling by the client when possible, but Microsoft Graph does not expose a pre-authenticated download URL for this resource."
|
|
1885
1908
|
},
|
|
1886
1909
|
{
|
|
1887
1910
|
"pathPattern": "/me/onlineMeetings/{onlineMeeting-id}/attendanceReports",
|