@softeria/ms-365-mcp-server 0.122.0 → 0.124.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 +44 -3
- package/dist/__tests__/graph-tools.test.js +0 -114
- package/dist/auth.js +53 -12
- package/dist/endpoints.json +42 -0
- package/dist/generated/client.js +69 -0
- package/dist/graph-tools.js +11 -95
- package/package.json +1 -1
- package/src/endpoints.json +42 -0
|
@@ -17,11 +17,27 @@ export function createAndSaveSimplifiedOpenAPI(endpointsFile, openapiFile, opena
|
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
//
|
|
20
|
+
// Two cases handled here:
|
|
21
|
+
// 1. The method is missing entirely (Microsoft never published this operation):
|
|
22
|
+
// synthesize the whole operation from endpoints.json.
|
|
23
|
+
// 2. The method exists but the endpoint declares its own requestBodySchema
|
|
24
|
+
// (Microsoft published a deprecated/malformed/private-preview body our generator
|
|
25
|
+
// can't consume): override ONLY the request body and keep Microsoft's published
|
|
26
|
+
// responses/parameters intact, so we don't silently drop upstream fields or mask
|
|
27
|
+
// a future upstream fix. requestBodySchema is explicit opt-in per endpoint.
|
|
21
28
|
for (const endpoint of endpoints) {
|
|
22
29
|
const pathSpec = openApiSpec.paths[endpoint.pathPattern];
|
|
23
30
|
const methodLower = endpoint.method.toLowerCase();
|
|
24
|
-
if (
|
|
31
|
+
if (!pathSpec) continue;
|
|
32
|
+
|
|
33
|
+
const operationMissing = !pathSpec[methodLower];
|
|
34
|
+
const overrideBodyOnly =
|
|
35
|
+
!operationMissing &&
|
|
36
|
+
endpoint.requestBodySchema &&
|
|
37
|
+
methodLower !== 'get' &&
|
|
38
|
+
methodLower !== 'delete';
|
|
39
|
+
|
|
40
|
+
if (operationMissing) {
|
|
25
41
|
const pathParamMatches = [...endpoint.pathPattern.matchAll(/\{([^}]+)\}/g)].map((m) => m[1]);
|
|
26
42
|
const synthesizedParameters = pathParamMatches.map((paramName) => ({
|
|
27
43
|
name: paramName,
|
|
@@ -44,7 +60,14 @@ export function createAndSaveSimplifiedOpenAPI(endpointsFile, openapiFile, opena
|
|
|
44
60
|
required: true,
|
|
45
61
|
content: {
|
|
46
62
|
'application/json': {
|
|
47
|
-
|
|
63
|
+
// When an endpoint declares a typed body schema in endpoints.json
|
|
64
|
+
// (for APIs Microsoft hasn't published in its OpenAPI metadata),
|
|
65
|
+
// use it so the generated client gets a validated `body` param.
|
|
66
|
+
// Otherwise fall back to a permissive object.
|
|
67
|
+
schema: endpoint.requestBodySchema ?? {
|
|
68
|
+
type: 'object',
|
|
69
|
+
additionalProperties: true,
|
|
70
|
+
},
|
|
48
71
|
},
|
|
49
72
|
},
|
|
50
73
|
},
|
|
@@ -61,6 +84,24 @@ export function createAndSaveSimplifiedOpenAPI(endpointsFile, openapiFile, opena
|
|
|
61
84
|
'5XX': { $ref: '#/components/responses/error' },
|
|
62
85
|
},
|
|
63
86
|
};
|
|
87
|
+
} else if (overrideBodyOnly) {
|
|
88
|
+
// Surgical override: replace only the request body, leaving Microsoft's
|
|
89
|
+
// published responses and parameters for this operation untouched.
|
|
90
|
+
pathSpec[methodLower].requestBody = {
|
|
91
|
+
description: pathSpec[methodLower].requestBody?.description || 'Operation payload',
|
|
92
|
+
required: true,
|
|
93
|
+
content: {
|
|
94
|
+
'application/json': {
|
|
95
|
+
schema: endpoint.requestBodySchema,
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
// These operations are often published as deprecated/private-preview, which
|
|
100
|
+
// openapi-zod-client skips by default. Declaring a requestBodySchema means we
|
|
101
|
+
// deliberately vouch for the endpoint, so clear the deprecation markers to keep
|
|
102
|
+
// it in the generated client.
|
|
103
|
+
delete pathSpec[methodLower].deprecated;
|
|
104
|
+
delete pathSpec[methodLower]['x-ms-deprecation'];
|
|
64
105
|
}
|
|
65
106
|
}
|
|
66
107
|
|
|
@@ -914,118 +914,4 @@ describe("graph-tools", () => {
|
|
|
914
914
|
expect(server.tools.has("parse-teams-url")).toBe(true);
|
|
915
915
|
});
|
|
916
916
|
});
|
|
917
|
-
describe("copilot-retrieve", () => {
|
|
918
|
-
const retrievalResponse = {
|
|
919
|
-
content: [
|
|
920
|
-
{
|
|
921
|
-
type: "text",
|
|
922
|
-
text: JSON.stringify({
|
|
923
|
-
retrievalHits: [
|
|
924
|
-
{
|
|
925
|
-
webUrl: "https://contoso.sharepoint.com/sites/HR/VPNAccess.docx",
|
|
926
|
-
extracts: [{ text: "To configure the VPN...", relevanceScore: 0.83 }],
|
|
927
|
-
resourceType: "listItem",
|
|
928
|
-
resourceMetadata: { title: "VPN Access" }
|
|
929
|
-
}
|
|
930
|
-
]
|
|
931
|
-
})
|
|
932
|
-
}
|
|
933
|
-
]
|
|
934
|
-
};
|
|
935
|
-
it("POSTs queryString + dataSource to /copilot/retrieval on v1.0 and returns the hits", async () => {
|
|
936
|
-
mockEndpoints.length = 0;
|
|
937
|
-
mockEndpointsJson = [];
|
|
938
|
-
const graphClient = { graphRequest: vi.fn().mockResolvedValue(retrievalResponse) };
|
|
939
|
-
const server = createMockServer();
|
|
940
|
-
const { registerGraphTools } = await loadModule();
|
|
941
|
-
registerGraphTools(server, graphClient);
|
|
942
|
-
const tool = server.tools.get("copilot-retrieve");
|
|
943
|
-
expect(tool).toBeDefined();
|
|
944
|
-
const result = await tool.handler({
|
|
945
|
-
queryString: "How to setup corporate VPN?",
|
|
946
|
-
dataSource: "sharePoint"
|
|
947
|
-
});
|
|
948
|
-
expect(graphClient.graphRequest).toHaveBeenCalledTimes(1);
|
|
949
|
-
const [endpoint, options] = graphClient.graphRequest.mock.calls[0];
|
|
950
|
-
expect(endpoint).toBe("/copilot/retrieval");
|
|
951
|
-
expect(options.method).toBe("POST");
|
|
952
|
-
expect(options.apiVersion === void 0 || options.apiVersion === "v1.0").toBe(true);
|
|
953
|
-
const body = JSON.parse(options.body);
|
|
954
|
-
expect(body).toEqual({
|
|
955
|
-
queryString: "How to setup corporate VPN?",
|
|
956
|
-
dataSource: "sharePoint"
|
|
957
|
-
});
|
|
958
|
-
const payload = JSON.parse(result.content[0].text);
|
|
959
|
-
expect(payload.retrievalHits[0].webUrl).toBe(
|
|
960
|
-
"https://contoso.sharepoint.com/sites/HR/VPNAccess.docx"
|
|
961
|
-
);
|
|
962
|
-
});
|
|
963
|
-
it("includes optional filterExpression, resourceMetadata, and maximumNumberOfResults only when provided", async () => {
|
|
964
|
-
mockEndpoints.length = 0;
|
|
965
|
-
mockEndpointsJson = [];
|
|
966
|
-
const graphClient = { graphRequest: vi.fn().mockResolvedValue(retrievalResponse) };
|
|
967
|
-
const server = createMockServer();
|
|
968
|
-
const { registerGraphTools } = await loadModule();
|
|
969
|
-
registerGraphTools(server, graphClient);
|
|
970
|
-
const tool = server.tools.get("copilot-retrieve");
|
|
971
|
-
await tool.handler({
|
|
972
|
-
queryString: "corporate VPN",
|
|
973
|
-
dataSource: "sharePoint",
|
|
974
|
-
filterExpression: 'Author:"Megan Bowen"',
|
|
975
|
-
resourceMetadata: ["title", "author"],
|
|
976
|
-
maximumNumberOfResults: 5
|
|
977
|
-
});
|
|
978
|
-
const [, options] = graphClient.graphRequest.mock.calls[0];
|
|
979
|
-
const body = JSON.parse(options.body);
|
|
980
|
-
expect(body.filterExpression).toBe('Author:"Megan Bowen"');
|
|
981
|
-
expect(body.resourceMetadata).toEqual(["title", "author"]);
|
|
982
|
-
expect(body.maximumNumberOfResults).toBe(5);
|
|
983
|
-
});
|
|
984
|
-
it("requires a non-empty queryString", async () => {
|
|
985
|
-
mockEndpoints.length = 0;
|
|
986
|
-
mockEndpointsJson = [];
|
|
987
|
-
const graphClient = { graphRequest: vi.fn() };
|
|
988
|
-
const server = createMockServer();
|
|
989
|
-
const { registerGraphTools } = await loadModule();
|
|
990
|
-
registerGraphTools(server, graphClient);
|
|
991
|
-
const tool = server.tools.get("copilot-retrieve");
|
|
992
|
-
const result = await tool.handler({ queryString: " ", dataSource: "sharePoint" });
|
|
993
|
-
expect(result.isError).toBe(true);
|
|
994
|
-
expect(graphClient.graphRequest).not.toHaveBeenCalled();
|
|
995
|
-
const payload = JSON.parse(result.content[0].text);
|
|
996
|
-
expect(payload.error).toMatch(/queryString/);
|
|
997
|
-
});
|
|
998
|
-
it("rejects an invalid dataSource", async () => {
|
|
999
|
-
mockEndpoints.length = 0;
|
|
1000
|
-
mockEndpointsJson = [];
|
|
1001
|
-
const graphClient = { graphRequest: vi.fn() };
|
|
1002
|
-
const server = createMockServer();
|
|
1003
|
-
const { registerGraphTools } = await loadModule();
|
|
1004
|
-
registerGraphTools(server, graphClient);
|
|
1005
|
-
const tool = server.tools.get("copilot-retrieve");
|
|
1006
|
-
const result = await tool.handler({ queryString: "vpn", dataSource: "mailbox" });
|
|
1007
|
-
expect(result.isError).toBe(true);
|
|
1008
|
-
expect(graphClient.graphRequest).not.toHaveBeenCalled();
|
|
1009
|
-
const payload = JSON.parse(result.content[0].text);
|
|
1010
|
-
expect(payload.error).toMatch(/dataSource/);
|
|
1011
|
-
});
|
|
1012
|
-
it("rejects maximumNumberOfResults outside the 1-25 range", async () => {
|
|
1013
|
-
mockEndpoints.length = 0;
|
|
1014
|
-
mockEndpointsJson = [];
|
|
1015
|
-
const graphClient = { graphRequest: vi.fn() };
|
|
1016
|
-
const server = createMockServer();
|
|
1017
|
-
const { registerGraphTools } = await loadModule();
|
|
1018
|
-
registerGraphTools(server, graphClient);
|
|
1019
|
-
const tool = server.tools.get("copilot-retrieve");
|
|
1020
|
-
const result = await tool.handler({
|
|
1021
|
-
queryString: "vpn",
|
|
1022
|
-
dataSource: "sharePoint",
|
|
1023
|
-
maximumNumberOfResults: 50
|
|
1024
|
-
});
|
|
1025
|
-
expect(result.isError).toBe(true);
|
|
1026
|
-
expect(graphClient.graphRequest).not.toHaveBeenCalled();
|
|
1027
|
-
const payload = JSON.parse(result.content[0].text);
|
|
1028
|
-
expect(payload.error).toMatch(/maximumNumberOfResults/);
|
|
1029
|
-
});
|
|
1030
|
-
});
|
|
1031
917
|
});
|
package/dist/auth.js
CHANGED
|
@@ -49,13 +49,47 @@ function getEndpointRequiredScopes(endpoint, includeWorkAccountScopes = false) {
|
|
|
49
49
|
return [];
|
|
50
50
|
}
|
|
51
51
|
const scopes = /* @__PURE__ */ new Set();
|
|
52
|
-
|
|
53
|
-
|
|
52
|
+
getEndpointScopeGroups(endpoint, includeWorkAccountScopes).forEach(
|
|
53
|
+
(group) => group.forEach((scope) => scopes.add(scope))
|
|
54
|
+
);
|
|
55
|
+
return Array.from(scopes);
|
|
56
|
+
}
|
|
57
|
+
function toScopeGroups(value) {
|
|
58
|
+
if (!value || value.length === 0) {
|
|
59
|
+
return [];
|
|
54
60
|
}
|
|
55
|
-
|
|
56
|
-
|
|
61
|
+
return Array.isArray(value[0]) ? value : [value];
|
|
62
|
+
}
|
|
63
|
+
function getEndpointScopeGroups(endpoint, includeWorkAccountScopes = false) {
|
|
64
|
+
if (!endpoint) {
|
|
65
|
+
return [];
|
|
57
66
|
}
|
|
58
|
-
|
|
67
|
+
const groups = [...toScopeGroups(endpoint.scopes)];
|
|
68
|
+
if (includeWorkAccountScopes) {
|
|
69
|
+
groups.push(...toScopeGroups(endpoint.workScopes));
|
|
70
|
+
}
|
|
71
|
+
return groups;
|
|
72
|
+
}
|
|
73
|
+
function getEndpointLoginScopes(endpoint, includeWorkAccountScopes = false) {
|
|
74
|
+
const groups = getEndpointScopeGroups(endpoint, includeWorkAccountScopes);
|
|
75
|
+
return groups.length > 0 ? groups[0] : [];
|
|
76
|
+
}
|
|
77
|
+
function getMissingAllowedScopesForGroups(scopeGroups, allowedScopes) {
|
|
78
|
+
if (allowedScopes === void 0 || scopeGroups.length === 0) {
|
|
79
|
+
return [];
|
|
80
|
+
}
|
|
81
|
+
const coveredAllowedScopes = new Set(collapseScopeHierarchy(allowedScopes));
|
|
82
|
+
let closest;
|
|
83
|
+
for (const group of scopeGroups) {
|
|
84
|
+
const missing = group.filter((scope) => !coveredAllowedScopes.has(scope));
|
|
85
|
+
if (missing.length === 0) {
|
|
86
|
+
return [];
|
|
87
|
+
}
|
|
88
|
+
if (!closest || missing.length < closest.length) {
|
|
89
|
+
closest = missing;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return closest ?? [];
|
|
59
93
|
}
|
|
60
94
|
function collapseRedundantScopes(scopes) {
|
|
61
95
|
const scopesSet = new Set(scopes);
|
|
@@ -91,7 +125,7 @@ function buildScopesFromEndpoints(includeWorkAccountScopes = false, enabledTools
|
|
|
91
125
|
if (!includeWorkAccountScopes && !endpoint.scopes && endpoint.workScopes) {
|
|
92
126
|
return;
|
|
93
127
|
}
|
|
94
|
-
|
|
128
|
+
getEndpointLoginScopes(endpoint, includeWorkAccountScopes).forEach(
|
|
95
129
|
(scope) => scopesSet.add(scope)
|
|
96
130
|
);
|
|
97
131
|
});
|
|
@@ -173,6 +207,7 @@ function buildAllowedScopeDiagnostics(options = {}) {
|
|
|
173
207
|
}
|
|
174
208
|
const normalToolScopes = /* @__PURE__ */ new Set();
|
|
175
209
|
const effectiveToolScopes = /* @__PURE__ */ new Set();
|
|
210
|
+
const effectiveToolScopesAllGroups = /* @__PURE__ */ new Set();
|
|
176
211
|
const disabledTools = [];
|
|
177
212
|
for (const endpoint of endpoints.default) {
|
|
178
213
|
if (!endpointMatchesNormalToolSurface(
|
|
@@ -183,18 +218,21 @@ function buildAllowedScopeDiagnostics(options = {}) {
|
|
|
183
218
|
)) {
|
|
184
219
|
continue;
|
|
185
220
|
}
|
|
186
|
-
const
|
|
187
|
-
|
|
188
|
-
const
|
|
221
|
+
const scopeGroups = getEndpointScopeGroups(endpoint, Boolean(options.orgMode));
|
|
222
|
+
const loginScopes = getEndpointLoginScopes(endpoint, Boolean(options.orgMode));
|
|
223
|
+
const allScopes = getEndpointRequiredScopes(endpoint, Boolean(options.orgMode));
|
|
224
|
+
loginScopes.forEach((scope) => normalToolScopes.add(scope));
|
|
225
|
+
const missingScopes = getMissingAllowedScopesForGroups(scopeGroups, allowedScopes);
|
|
189
226
|
if (missingScopes.length > 0) {
|
|
190
227
|
disabledTools.push({
|
|
191
228
|
toolName: endpoint.toolName,
|
|
192
|
-
requiredScopes:
|
|
229
|
+
requiredScopes: allScopes.sort((a, b) => a.localeCompare(b)),
|
|
193
230
|
missingScopes: missingScopes.sort((a, b) => a.localeCompare(b))
|
|
194
231
|
});
|
|
195
232
|
continue;
|
|
196
233
|
}
|
|
197
|
-
|
|
234
|
+
loginScopes.forEach((scope) => effectiveToolScopes.add(scope));
|
|
235
|
+
allScopes.forEach((scope) => effectiveToolScopesAllGroups.add(scope));
|
|
198
236
|
}
|
|
199
237
|
const toolPermissions = collapseRedundantScopes(Array.from(normalToolScopes)).sort(
|
|
200
238
|
(a, b) => a.localeCompare(b)
|
|
@@ -206,7 +244,8 @@ function buildAllowedScopeDiagnostics(options = {}) {
|
|
|
206
244
|
const missingAllowedScopesForTools = Array.from(
|
|
207
245
|
new Set(disabledTools.flatMap((tool) => tool.missingScopes))
|
|
208
246
|
).sort((a, b) => a.localeCompare(b));
|
|
209
|
-
const
|
|
247
|
+
const allEffectiveToolScopes = Array.from(effectiveToolScopesAllGroups);
|
|
248
|
+
const extraAllowedScopesNotUsedByTools = sortedAllowedScopes?.filter((scope) => !isScopeUsedByTools(scope, allEffectiveToolScopes)) ?? [];
|
|
210
249
|
return {
|
|
211
250
|
permissions: effectivePermissions,
|
|
212
251
|
toolPermissions,
|
|
@@ -811,7 +850,9 @@ export {
|
|
|
811
850
|
auth_default as default,
|
|
812
851
|
describeAuthError,
|
|
813
852
|
getEndpointRequiredScopes,
|
|
853
|
+
getEndpointScopeGroups,
|
|
814
854
|
getMissingAllowedScopes,
|
|
855
|
+
getMissingAllowedScopesForGroups,
|
|
815
856
|
getSelectedAccountPath,
|
|
816
857
|
getTokenCachePath,
|
|
817
858
|
parseAllowedScopes,
|
package/dist/endpoints.json
CHANGED
|
@@ -1742,6 +1742,48 @@
|
|
|
1742
1742
|
"ChannelMessage.Read.All"
|
|
1743
1743
|
]
|
|
1744
1744
|
},
|
|
1745
|
+
{
|
|
1746
|
+
"pathPattern": "/copilot/retrieval",
|
|
1747
|
+
"method": "post",
|
|
1748
|
+
"toolName": "copilot-retrieve",
|
|
1749
|
+
"presets": ["search", "work"],
|
|
1750
|
+
"readOnly": true,
|
|
1751
|
+
"workScopes": [["Files.Read.All", "Sites.Read.All"], ["ExternalItem.Read.All"]],
|
|
1752
|
+
"requestBodySchema": {
|
|
1753
|
+
"type": "object",
|
|
1754
|
+
"required": ["queryString", "dataSource"],
|
|
1755
|
+
"properties": {
|
|
1756
|
+
"queryString": {
|
|
1757
|
+
"type": "string",
|
|
1758
|
+
"minLength": 1,
|
|
1759
|
+
"maxLength": 1500,
|
|
1760
|
+
"pattern": "\\S",
|
|
1761
|
+
"description": "Natural-language query (a single sentence works best; max 1500 characters; must contain a non-whitespace character). Avoid spelling errors in context-rich keywords."
|
|
1762
|
+
},
|
|
1763
|
+
"dataSource": {
|
|
1764
|
+
"type": "string",
|
|
1765
|
+
"enum": ["sharePoint", "oneDriveBusiness", "externalItem"],
|
|
1766
|
+
"description": "Which source to retrieve from — one at a time (interleaved results are not supported). 'sharePoint', 'oneDriveBusiness', or 'externalItem' (Copilot connectors)."
|
|
1767
|
+
},
|
|
1768
|
+
"filterExpression": {
|
|
1769
|
+
"type": "string",
|
|
1770
|
+
"description": "Optional KQL expression to scope the retrieval before the query runs. Supported SharePoint/OneDrive properties: Author, FileExtension, Filename, FileType, InformationProtectionLabelId, LastModifiedTime, ModifiedBy, Path, SiteID, Title. Invalid KQL is ignored (query runs unscoped)."
|
|
1771
|
+
},
|
|
1772
|
+
"resourceMetadata": {
|
|
1773
|
+
"type": "array",
|
|
1774
|
+
"items": { "type": "string" },
|
|
1775
|
+
"description": "Optional list of retrievable metadata fields to return per hit (e.g. ['title', 'author']). By default no metadata is returned."
|
|
1776
|
+
},
|
|
1777
|
+
"maximumNumberOfResults": {
|
|
1778
|
+
"type": "integer",
|
|
1779
|
+
"minimum": 1,
|
|
1780
|
+
"maximum": 25,
|
|
1781
|
+
"description": "Optional cap on the number of results (1-25). Best practice: leave unset unless your LLM has strict token limits — results are unordered."
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
},
|
|
1785
|
+
"llmTip": "Semantic/hybrid search over Microsoft 365 content via the Copilot Retrieval API. Grounds a natural-language query against the same hybrid index that powers Microsoft 365 Copilot and returns permission-trimmed text extracts with their source URLs — unlike search-query/search-onedrive-files/search-sharepoint-sites, which are lexical KQL. Prefer this for paraphrase/intent queries (e.g. resolving an informal project nickname, or matching 'packaging requirements' against text that never uses that phrase). Pass the request fields nested under a 'body' object: { queryString, dataSource, filterExpression?, resourceMetadata?, maximumNumberOfResults? }. Returns { retrievalHits: [{ webUrl, extracts: [{ text, relevanceScore }], resourceType, resourceMetadata, sensitivityLabel? }] }. Work/school accounts only (personal accounts are not supported). dataSource 'sharePoint'/'oneDriveBusiness' need delegated Files.Read.All + Sites.Read.All (requested by default); dataSource 'externalItem' (Copilot connectors) needs ExternalItem.Read.All, which is not requested by default - add it with --extra-scopes ExternalItem.Read.All."
|
|
1786
|
+
},
|
|
1745
1787
|
{
|
|
1746
1788
|
"pathPattern": "/me/onlineMeetings",
|
|
1747
1789
|
"method": "get",
|
package/dist/generated/client.js
CHANGED
|
@@ -547,6 +547,52 @@ const microsoft_graph_presence = z.object({
|
|
|
547
547
|
statusMessage: microsoft_graph_presenceStatusMessage.optional(),
|
|
548
548
|
workLocation: microsoft_graph_userWorkLocation.optional()
|
|
549
549
|
}).passthrough();
|
|
550
|
+
const copilot_retrieve_Body = z.object({
|
|
551
|
+
queryString: z.string().min(1).max(1500).regex(/\S/).describe(
|
|
552
|
+
"Natural-language query (a single sentence works best; max 1500 characters; must contain a non-whitespace character). Avoid spelling errors in context-rich keywords."
|
|
553
|
+
),
|
|
554
|
+
dataSource: z.enum(["sharePoint", "oneDriveBusiness", "externalItem"]).describe(
|
|
555
|
+
"Which source to retrieve from \u2014 one at a time (interleaved results are not supported). 'sharePoint', 'oneDriveBusiness', or 'externalItem' (Copilot connectors)."
|
|
556
|
+
),
|
|
557
|
+
filterExpression: z.string().describe(
|
|
558
|
+
"Optional KQL expression to scope the retrieval before the query runs. Supported SharePoint/OneDrive properties: Author, FileExtension, Filename, FileType, InformationProtectionLabelId, LastModifiedTime, ModifiedBy, Path, SiteID, Title. Invalid KQL is ignored (query runs unscoped)."
|
|
559
|
+
).optional(),
|
|
560
|
+
resourceMetadata: z.array(z.string()).describe(
|
|
561
|
+
"Optional list of retrievable metadata fields to return per hit (e.g. ['title', 'author']). By default no metadata is returned."
|
|
562
|
+
).optional(),
|
|
563
|
+
maximumNumberOfResults: z.number().int().gte(1).lte(25).describe(
|
|
564
|
+
"Optional cap on the number of results (1-25). Best practice: leave unset unless your LLM has strict token limits \u2014 results are unordered."
|
|
565
|
+
).optional()
|
|
566
|
+
}).passthrough();
|
|
567
|
+
const microsoft_graph_retrievalExtract = z.object({
|
|
568
|
+
relevanceScore: z.number().describe("[Simplified from 3 options]").nullish(),
|
|
569
|
+
text: z.string().nullish()
|
|
570
|
+
}).passthrough();
|
|
571
|
+
const microsoft_graph_searchResourceMetadataDictionary = z.object({}).passthrough();
|
|
572
|
+
const microsoft_graph_retrievalEntityType = z.enum([
|
|
573
|
+
"site",
|
|
574
|
+
"list",
|
|
575
|
+
"listItem",
|
|
576
|
+
"drive",
|
|
577
|
+
"driveItem",
|
|
578
|
+
"externalItem",
|
|
579
|
+
"unknownFutureValue"
|
|
580
|
+
]);
|
|
581
|
+
const microsoft_graph_sensitivityLabelInfo = z.object({
|
|
582
|
+
color: z.string().nullish(),
|
|
583
|
+
displayName: z.string().nullish(),
|
|
584
|
+
priority: z.number().gte(-2147483648).lte(2147483647).nullish(),
|
|
585
|
+
sensitivityLabelId: z.string().nullish(),
|
|
586
|
+
tooltip: z.string().nullish()
|
|
587
|
+
}).passthrough();
|
|
588
|
+
const microsoft_graph_retrievalHit = z.object({
|
|
589
|
+
extracts: z.array(microsoft_graph_retrievalExtract).optional(),
|
|
590
|
+
resourceMetadata: microsoft_graph_searchResourceMetadataDictionary.optional(),
|
|
591
|
+
resourceType: microsoft_graph_retrievalEntityType.optional(),
|
|
592
|
+
sensitivityLabel: microsoft_graph_sensitivityLabelInfo.optional(),
|
|
593
|
+
webUrl: z.string().nullish()
|
|
594
|
+
}).passthrough();
|
|
595
|
+
const microsoft_graph_retrievalResponse = z.object({ retrievalHits: z.array(microsoft_graph_retrievalHit).optional() }).passthrough();
|
|
550
596
|
const microsoft_graph_geoCoordinates = z.object({
|
|
551
597
|
altitude: z.number().describe(
|
|
552
598
|
"Optional. The altitude (height), in feet, above sea level for the item. Read-only. [Simplified from 3 options]"
|
|
@@ -4818,6 +4864,13 @@ const schemas = {
|
|
|
4818
4864
|
microsoft_graph_workLocationType,
|
|
4819
4865
|
microsoft_graph_userWorkLocation,
|
|
4820
4866
|
microsoft_graph_presence,
|
|
4867
|
+
copilot_retrieve_Body,
|
|
4868
|
+
microsoft_graph_retrievalExtract,
|
|
4869
|
+
microsoft_graph_searchResourceMetadataDictionary,
|
|
4870
|
+
microsoft_graph_retrievalEntityType,
|
|
4871
|
+
microsoft_graph_sensitivityLabelInfo,
|
|
4872
|
+
microsoft_graph_retrievalHit,
|
|
4873
|
+
microsoft_graph_retrievalResponse,
|
|
4821
4874
|
microsoft_graph_geoCoordinates,
|
|
4822
4875
|
microsoft_graph_sharepointIds,
|
|
4823
4876
|
microsoft_graph_itemReference,
|
|
@@ -5683,6 +5736,22 @@ const endpoints = makeApi([
|
|
|
5683
5736
|
],
|
|
5684
5737
|
response: z.void()
|
|
5685
5738
|
},
|
|
5739
|
+
{
|
|
5740
|
+
method: "post",
|
|
5741
|
+
path: "/copilot/retrieval",
|
|
5742
|
+
alias: "copilot-retrieve",
|
|
5743
|
+
description: `Invoke action retrieval`,
|
|
5744
|
+
requestFormat: "json",
|
|
5745
|
+
parameters: [
|
|
5746
|
+
{
|
|
5747
|
+
name: "body",
|
|
5748
|
+
description: `Action parameters`,
|
|
5749
|
+
type: "Body",
|
|
5750
|
+
schema: copilot_retrieve_Body
|
|
5751
|
+
}
|
|
5752
|
+
],
|
|
5753
|
+
response: z.void()
|
|
5754
|
+
},
|
|
5686
5755
|
{
|
|
5687
5756
|
method: "get",
|
|
5688
5757
|
path: "/drives/:driveId/items/:driveItemId",
|
package/dist/graph-tools.js
CHANGED
|
@@ -2,8 +2,8 @@ import { randomUUID } from "crypto";
|
|
|
2
2
|
import logger from "./logger.js";
|
|
3
3
|
import { auditLog, getUserIdentityForAudit } from "./audit-log.js";
|
|
4
4
|
import {
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
getEndpointScopeGroups,
|
|
6
|
+
getMissingAllowedScopesForGroups,
|
|
7
7
|
parseAllowedScopes
|
|
8
8
|
} from "./auth.js";
|
|
9
9
|
import { api } from "./generated/client.js";
|
|
@@ -169,93 +169,6 @@ const UTILITY_TOOLS = [
|
|
|
169
169
|
};
|
|
170
170
|
}
|
|
171
171
|
}
|
|
172
|
-
},
|
|
173
|
-
{
|
|
174
|
-
name: "copilot-retrieve",
|
|
175
|
-
method: "POST",
|
|
176
|
-
path: "tool:copilot-retrieve",
|
|
177
|
-
description: 'Semantic search over Microsoft 365 content via the Copilot Retrieval API (POST /copilot/retrieval). Grounds a natural-language query against the same hybrid index that powers Microsoft 365 Copilot and returns relevant, permission-trimmed text extracts with their source URLs \u2014 unlike search-query/search-onedrive-files/search-sharepoint-sites, which are lexical KQL. Prefer this for paraphrase/intent queries (e.g. resolving an informal project nickname, or matching "packaging requirements" against text that never uses that phrase). Returns { retrievalHits: [{ webUrl, extracts: [{ text, relevanceScore }], resourceType, resourceMetadata, sensitivityLabel? }] }. Read-only. Requires delegated Files.Read.All + Sites.Read.All (SharePoint/OneDrive) or ExternalItem.Read.All (Copilot connectors); application-only auth is not supported.',
|
|
178
|
-
readOnlyHint: true,
|
|
179
|
-
openWorldHint: true,
|
|
180
|
-
buildSchema: (ctx) => {
|
|
181
|
-
const schema = {
|
|
182
|
-
queryString: z.string().min(1).max(1500).describe(
|
|
183
|
-
"Natural-language query (a single sentence works best; max 1500 characters). Avoid spelling errors in context-rich keywords."
|
|
184
|
-
),
|
|
185
|
-
dataSource: z.enum(["sharePoint", "oneDriveBusiness", "externalItem"]).describe(
|
|
186
|
-
'Which source to retrieve from \u2014 one at a time (interleaved results are not supported). "sharePoint", "oneDriveBusiness", or "externalItem" (Copilot connectors).'
|
|
187
|
-
),
|
|
188
|
-
filterExpression: z.string().optional().describe(
|
|
189
|
-
'Optional KQL expression to scope the retrieval before the query runs. Supported SharePoint/OneDrive properties: Author, FileExtension, Filename, FileType, InformationProtectionLabelId, LastModifiedTime, ModifiedBy, Path, SiteID, Title. Example: Author:"Megan Bowen" OR Path:"https://contoso.sharepoint.com/sites/HR/". Invalid KQL is ignored (query runs unscoped).'
|
|
190
|
-
),
|
|
191
|
-
resourceMetadata: z.array(z.string()).optional().describe(
|
|
192
|
-
'Optional list of retrievable metadata fields to return per hit (e.g. ["title", "author"]). By default no metadata is returned.'
|
|
193
|
-
),
|
|
194
|
-
maximumNumberOfResults: z.number().int().min(1).max(25).optional().describe(
|
|
195
|
-
"Optional cap on the number of results (1-25). Best practice: leave unset unless your LLM has strict token limits \u2014 results are unordered."
|
|
196
|
-
)
|
|
197
|
-
};
|
|
198
|
-
if (ctx.multiAccount) {
|
|
199
|
-
schema["account"] = z.string().optional().describe(
|
|
200
|
-
"Account to use when multiple Microsoft accounts are configured. Required when multiple accounts exist (see list-accounts)."
|
|
201
|
-
);
|
|
202
|
-
}
|
|
203
|
-
return schema;
|
|
204
|
-
},
|
|
205
|
-
execute: async (params, { graphClient, authManager }) => {
|
|
206
|
-
const err = (message) => ({
|
|
207
|
-
content: [{ type: "text", text: JSON.stringify({ error: message }) }],
|
|
208
|
-
isError: true
|
|
209
|
-
});
|
|
210
|
-
const queryString = typeof params.queryString === "string" ? params.queryString : "";
|
|
211
|
-
if (queryString.trim().length === 0) {
|
|
212
|
-
return err("queryString is required and must be a non-empty string.");
|
|
213
|
-
}
|
|
214
|
-
if (queryString.length > 1500) {
|
|
215
|
-
return err("queryString must be 1500 characters or fewer.");
|
|
216
|
-
}
|
|
217
|
-
const dataSource = params.dataSource;
|
|
218
|
-
const allowedSources = ["sharePoint", "oneDriveBusiness", "externalItem"];
|
|
219
|
-
if (typeof dataSource !== "string" || !allowedSources.includes(dataSource)) {
|
|
220
|
-
return err(`dataSource is required and must be one of: ${allowedSources.join(", ")}.`);
|
|
221
|
-
}
|
|
222
|
-
const body = { queryString, dataSource };
|
|
223
|
-
if (params.filterExpression !== void 0) {
|
|
224
|
-
if (typeof params.filterExpression !== "string") {
|
|
225
|
-
return err("filterExpression must be a string (KQL expression).");
|
|
226
|
-
}
|
|
227
|
-
body.filterExpression = params.filterExpression;
|
|
228
|
-
}
|
|
229
|
-
if (params.resourceMetadata !== void 0) {
|
|
230
|
-
const rm = params.resourceMetadata;
|
|
231
|
-
if (!Array.isArray(rm) || rm.some((x) => typeof x !== "string")) {
|
|
232
|
-
return err("resourceMetadata must be an array of strings.");
|
|
233
|
-
}
|
|
234
|
-
body.resourceMetadata = rm;
|
|
235
|
-
}
|
|
236
|
-
if (params.maximumNumberOfResults !== void 0) {
|
|
237
|
-
const n = params.maximumNumberOfResults;
|
|
238
|
-
if (typeof n !== "number" || !Number.isInteger(n) || n < 1 || n > 25) {
|
|
239
|
-
return err("maximumNumberOfResults must be an integer between 1 and 25.");
|
|
240
|
-
}
|
|
241
|
-
body.maximumNumberOfResults = n;
|
|
242
|
-
}
|
|
243
|
-
try {
|
|
244
|
-
let accountAccessToken;
|
|
245
|
-
if (authManager && !authManager.isOAuthModeEnabled() && !getRequestTokens()) {
|
|
246
|
-
accountAccessToken = await authManager.getTokenForAccount(
|
|
247
|
-
params.account
|
|
248
|
-
);
|
|
249
|
-
}
|
|
250
|
-
return await graphClient.graphRequest("/copilot/retrieval", {
|
|
251
|
-
method: "POST",
|
|
252
|
-
body: JSON.stringify(body),
|
|
253
|
-
accessToken: accountAccessToken
|
|
254
|
-
});
|
|
255
|
-
} catch (error) {
|
|
256
|
-
return err(error.message);
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
172
|
}
|
|
260
173
|
];
|
|
261
174
|
function registerUtilityToolWithMcp(server, utility, ctx) {
|
|
@@ -609,8 +522,10 @@ function registerGraphTools(server, graphClient, readOnly = false, enabledToolsP
|
|
|
609
522
|
skippedCount++;
|
|
610
523
|
continue;
|
|
611
524
|
}
|
|
612
|
-
const
|
|
613
|
-
|
|
525
|
+
const missingScopes = allowedScopes !== void 0 && !endpointConfig ? ["endpoint scope metadata"] : getMissingAllowedScopesForGroups(
|
|
526
|
+
getEndpointScopeGroups(endpointConfig, orgMode),
|
|
527
|
+
allowedScopes
|
|
528
|
+
);
|
|
614
529
|
if (missingScopes.length > 0) {
|
|
615
530
|
disabledByAllowedScopes.push({ toolName: tool.alias, missingScopes });
|
|
616
531
|
skippedCount++;
|
|
@@ -693,6 +608,7 @@ function registerGraphTools(server, graphClient, readOnly = false, enabledToolsP
|
|
|
693
608
|
|
|
694
609
|
\u{1F4A1} TIP: ${endpointConfig.llmTip}`;
|
|
695
610
|
}
|
|
611
|
+
const isReadOnlyTool = tool.method.toUpperCase() === "GET" || endpointConfig?.readOnly === true;
|
|
696
612
|
try {
|
|
697
613
|
server.tool(
|
|
698
614
|
tool.alias,
|
|
@@ -700,8 +616,8 @@ function registerGraphTools(server, graphClient, readOnly = false, enabledToolsP
|
|
|
700
616
|
paramSchema,
|
|
701
617
|
{
|
|
702
618
|
title: tool.alias,
|
|
703
|
-
readOnlyHint:
|
|
704
|
-
destructiveHint: ["POST", "PATCH", "DELETE"].includes(tool.method.toUpperCase()),
|
|
619
|
+
readOnlyHint: isReadOnlyTool,
|
|
620
|
+
destructiveHint: !isReadOnlyTool && ["POST", "PATCH", "DELETE"].includes(tool.method.toUpperCase()),
|
|
705
621
|
openWorldHint: true
|
|
706
622
|
// All tools call Microsoft Graph API
|
|
707
623
|
},
|
|
@@ -760,8 +676,8 @@ function buildToolsRegistry(readOnly, orgMode, enabledToolsRegex, allowedScopesV
|
|
|
760
676
|
if (enabledToolsRegex && !enabledToolsRegex.test(tool.alias)) {
|
|
761
677
|
continue;
|
|
762
678
|
}
|
|
763
|
-
const missingScopes = allowedScopes !== void 0 && !endpointConfig ? ["endpoint scope metadata"] :
|
|
764
|
-
|
|
679
|
+
const missingScopes = allowedScopes !== void 0 && !endpointConfig ? ["endpoint scope metadata"] : getMissingAllowedScopesForGroups(
|
|
680
|
+
getEndpointScopeGroups(endpointConfig, orgMode),
|
|
765
681
|
allowedScopes
|
|
766
682
|
);
|
|
767
683
|
if (missingScopes.length > 0) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@softeria/ms-365-mcp-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.124.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
|
@@ -1742,6 +1742,48 @@
|
|
|
1742
1742
|
"ChannelMessage.Read.All"
|
|
1743
1743
|
]
|
|
1744
1744
|
},
|
|
1745
|
+
{
|
|
1746
|
+
"pathPattern": "/copilot/retrieval",
|
|
1747
|
+
"method": "post",
|
|
1748
|
+
"toolName": "copilot-retrieve",
|
|
1749
|
+
"presets": ["search", "work"],
|
|
1750
|
+
"readOnly": true,
|
|
1751
|
+
"workScopes": [["Files.Read.All", "Sites.Read.All"], ["ExternalItem.Read.All"]],
|
|
1752
|
+
"requestBodySchema": {
|
|
1753
|
+
"type": "object",
|
|
1754
|
+
"required": ["queryString", "dataSource"],
|
|
1755
|
+
"properties": {
|
|
1756
|
+
"queryString": {
|
|
1757
|
+
"type": "string",
|
|
1758
|
+
"minLength": 1,
|
|
1759
|
+
"maxLength": 1500,
|
|
1760
|
+
"pattern": "\\S",
|
|
1761
|
+
"description": "Natural-language query (a single sentence works best; max 1500 characters; must contain a non-whitespace character). Avoid spelling errors in context-rich keywords."
|
|
1762
|
+
},
|
|
1763
|
+
"dataSource": {
|
|
1764
|
+
"type": "string",
|
|
1765
|
+
"enum": ["sharePoint", "oneDriveBusiness", "externalItem"],
|
|
1766
|
+
"description": "Which source to retrieve from — one at a time (interleaved results are not supported). 'sharePoint', 'oneDriveBusiness', or 'externalItem' (Copilot connectors)."
|
|
1767
|
+
},
|
|
1768
|
+
"filterExpression": {
|
|
1769
|
+
"type": "string",
|
|
1770
|
+
"description": "Optional KQL expression to scope the retrieval before the query runs. Supported SharePoint/OneDrive properties: Author, FileExtension, Filename, FileType, InformationProtectionLabelId, LastModifiedTime, ModifiedBy, Path, SiteID, Title. Invalid KQL is ignored (query runs unscoped)."
|
|
1771
|
+
},
|
|
1772
|
+
"resourceMetadata": {
|
|
1773
|
+
"type": "array",
|
|
1774
|
+
"items": { "type": "string" },
|
|
1775
|
+
"description": "Optional list of retrievable metadata fields to return per hit (e.g. ['title', 'author']). By default no metadata is returned."
|
|
1776
|
+
},
|
|
1777
|
+
"maximumNumberOfResults": {
|
|
1778
|
+
"type": "integer",
|
|
1779
|
+
"minimum": 1,
|
|
1780
|
+
"maximum": 25,
|
|
1781
|
+
"description": "Optional cap on the number of results (1-25). Best practice: leave unset unless your LLM has strict token limits — results are unordered."
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
},
|
|
1785
|
+
"llmTip": "Semantic/hybrid search over Microsoft 365 content via the Copilot Retrieval API. Grounds a natural-language query against the same hybrid index that powers Microsoft 365 Copilot and returns permission-trimmed text extracts with their source URLs — unlike search-query/search-onedrive-files/search-sharepoint-sites, which are lexical KQL. Prefer this for paraphrase/intent queries (e.g. resolving an informal project nickname, or matching 'packaging requirements' against text that never uses that phrase). Pass the request fields nested under a 'body' object: { queryString, dataSource, filterExpression?, resourceMetadata?, maximumNumberOfResults? }. Returns { retrievalHits: [{ webUrl, extracts: [{ text, relevanceScore }], resourceType, resourceMetadata, sensitivityLabel? }] }. Work/school accounts only (personal accounts are not supported). dataSource 'sharePoint'/'oneDriveBusiness' need delegated Files.Read.All + Sites.Read.All (requested by default); dataSource 'externalItem' (Copilot connectors) needs ExternalItem.Read.All, which is not requested by default - add it with --extra-scopes ExternalItem.Read.All."
|
|
1786
|
+
},
|
|
1745
1787
|
{
|
|
1746
1788
|
"pathPattern": "/me/onlineMeetings",
|
|
1747
1789
|
"method": "get",
|