@softeria/ms-365-mcp-server 0.121.0 → 0.123.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/README.md +13 -0
- package/bin/modules/simplified-openapi.mjs +44 -3
- package/dist/__tests__/graph-tools.test.js +0 -114
- package/dist/auth.js +6 -1
- package/dist/cli.js +12 -0
- package/dist/endpoints.json +42 -0
- package/dist/generated/client.js +69 -0
- package/dist/graph-tools.js +3 -89
- package/package.json +1 -1
- package/src/endpoints.json +42 -0
package/README.md
CHANGED
|
@@ -150,6 +150,18 @@ Scope coverage is hierarchy-aware: for example, `Mail.ReadWrite` covers tools th
|
|
|
150
150
|
|
|
151
151
|
In HTTP mode, OAuth discovery advertises the effective filtered permissions so clients request the same consent surface. On-Behalf-Of mode (`--obo`) still advertises `api://<clientId>/access_as_user` for protected-resource metadata; `--allowed-scopes` does not override OBO.
|
|
152
152
|
|
|
153
|
+
### Requesting extra scopes
|
|
154
|
+
|
|
155
|
+
`--allowed-scopes` only ever _narrows_ the token request. To request a Graph scope that no bundled tool needs — for example to drive an endpoint via `graph-batch` — use `--extra-scopes` (or `MS365_MCP_EXTRA_SCOPES`). These scopes are appended verbatim to the token request, on top of the tool-derived scopes.
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
npx @softeria/ms-365-mcp-server \
|
|
159
|
+
--org-mode \
|
|
160
|
+
--extra-scopes 'CopilotPackages.ReadWrite.All'
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
This is for use with your own Azure app registration (`MS365_MCP_CLIENT_ID` / `MS365_MCP_CLIENT_SECRET`): the default Softeria app only declares a lean, fixed permission set, so request additional scopes against an app you control (your tenant admin consents to them there). CLI value takes precedence over the env var; an empty value fails at startup.
|
|
164
|
+
|
|
153
165
|
## Organization/Work Mode
|
|
154
166
|
|
|
155
167
|
To access work/school features (Teams, SharePoint, etc.), enable organization mode using any of these flags:
|
|
@@ -542,6 +554,7 @@ The following options can be used when running ms-365-mcp-server directly from t
|
|
|
542
554
|
--force-work-scopes Backwards compatibility alias for --org-mode (deprecated)
|
|
543
555
|
--cloud <type> Microsoft cloud environment: global (default) or china (21Vianet)
|
|
544
556
|
--allowed-scopes <scopes> Limit exposed tools to Graph scopes covered by this allowlist
|
|
557
|
+
--extra-scopes <scopes> Append additional Graph scopes to the token request (for use with your own app registration + graph-batch)
|
|
545
558
|
--expected-username <username> Require local MSAL auth to use this account username
|
|
546
559
|
--expected-home-account-id <id> Require local MSAL auth to use this exact homeAccountId
|
|
547
560
|
```
|
|
@@ -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
|
@@ -218,7 +218,12 @@ function buildAllowedScopeDiagnostics(options = {}) {
|
|
|
218
218
|
};
|
|
219
219
|
}
|
|
220
220
|
function resolveAuthScopes(options = {}) {
|
|
221
|
-
|
|
221
|
+
const toolScopes = buildAllowedScopeDiagnostics(options).effectivePermissions;
|
|
222
|
+
const extraScopes = parseAllowedScopes(options.extraScopes);
|
|
223
|
+
if (!extraScopes || extraScopes.length === 0) {
|
|
224
|
+
return toolScopes;
|
|
225
|
+
}
|
|
226
|
+
return Array.from(/* @__PURE__ */ new Set([...toolScopes, ...extraScopes]));
|
|
222
227
|
}
|
|
223
228
|
function buildScopeDiagnostics(toolScopes, allowedScopesInput) {
|
|
224
229
|
const toolPermissions = [...toolScopes].sort((a, b) => a.localeCompare(b));
|
package/dist/cli.js
CHANGED
|
@@ -26,6 +26,9 @@ program.name("ms-365-mcp-server").description("Microsoft 365 MCP Server").versio
|
|
|
26
26
|
).option(
|
|
27
27
|
"--allowed-scopes <scopes>",
|
|
28
28
|
"Limit exposed tools to Graph scopes covered by this whitespace-separated allowlist"
|
|
29
|
+
).option(
|
|
30
|
+
"--extra-scopes <scopes>",
|
|
31
|
+
"Append additional Graph scopes (whitespace-separated) to the token request, beyond those derived from enabled tools. Use with your own app registration (MS365_MCP_CLIENT_ID/SECRET) to request scopes the default app does not declare, then call the endpoints via graph-batch."
|
|
29
32
|
).option(
|
|
30
33
|
"--preset <names>",
|
|
31
34
|
"Use preset tool categories (comma-separated). Available: mail, calendar, files, personal, work, excel, contacts, tasks, onenote, search, users, all"
|
|
@@ -97,6 +100,15 @@ function parseArgs() {
|
|
|
97
100
|
);
|
|
98
101
|
process.exit(1);
|
|
99
102
|
}
|
|
103
|
+
if (options.extraScopes === void 0 && process.env.MS365_MCP_EXTRA_SCOPES !== void 0) {
|
|
104
|
+
options.extraScopes = process.env.MS365_MCP_EXTRA_SCOPES;
|
|
105
|
+
}
|
|
106
|
+
if (options.extraScopes !== void 0 && options.extraScopes.trim() === "") {
|
|
107
|
+
console.error(
|
|
108
|
+
"Error: --extra-scopes / MS365_MCP_EXTRA_SCOPES was provided but is empty. Provide one or more whitespace-separated scopes, or omit it."
|
|
109
|
+
);
|
|
110
|
+
process.exit(1);
|
|
111
|
+
}
|
|
100
112
|
if (options.expectedUsername === void 0 && process.env.MS365_MCP_EXPECTED_USERNAME !== void 0) {
|
|
101
113
|
options.expectedUsername = process.env.MS365_MCP_EXPECTED_USERNAME;
|
|
102
114
|
}
|
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); needs delegated Files.Read.All + Sites.Read.All (SharePoint/OneDrive) or ExternalItem.Read.All (Copilot connectors)."
|
|
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
|
@@ -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) {
|
|
@@ -693,6 +606,7 @@ function registerGraphTools(server, graphClient, readOnly = false, enabledToolsP
|
|
|
693
606
|
|
|
694
607
|
\u{1F4A1} TIP: ${endpointConfig.llmTip}`;
|
|
695
608
|
}
|
|
609
|
+
const isReadOnlyTool = tool.method.toUpperCase() === "GET" || endpointConfig?.readOnly === true;
|
|
696
610
|
try {
|
|
697
611
|
server.tool(
|
|
698
612
|
tool.alias,
|
|
@@ -700,8 +614,8 @@ function registerGraphTools(server, graphClient, readOnly = false, enabledToolsP
|
|
|
700
614
|
paramSchema,
|
|
701
615
|
{
|
|
702
616
|
title: tool.alias,
|
|
703
|
-
readOnlyHint:
|
|
704
|
-
destructiveHint: ["POST", "PATCH", "DELETE"].includes(tool.method.toUpperCase()),
|
|
617
|
+
readOnlyHint: isReadOnlyTool,
|
|
618
|
+
destructiveHint: !isReadOnlyTool && ["POST", "PATCH", "DELETE"].includes(tool.method.toUpperCase()),
|
|
705
619
|
openWorldHint: true
|
|
706
620
|
// All tools call Microsoft Graph API
|
|
707
621
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@softeria/ms-365-mcp-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.123.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); needs delegated Files.Read.All + Sites.Read.All (SharePoint/OneDrive) or ExternalItem.Read.All (Copilot connectors)."
|
|
1786
|
+
},
|
|
1745
1787
|
{
|
|
1746
1788
|
"pathPattern": "/me/onlineMeetings",
|
|
1747
1789
|
"method": "get",
|