@softeria/ms-365-mcp-server 0.119.0 → 0.120.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -285,7 +285,7 @@ Open WebUI supports MCP servers via HTTP transport with OAuth 2.1.
285
285
 
286
286
  3. Click **Register Client**.
287
287
 
288
- > **Note**: Dynamic client registration is enabled by default in HTTP mode. Use `--no-dynamic-registration` to disable it. If using a custom Azure Entra app, add your redirect URI under "Mobile and desktop applications" platform (not "Single-page application").
288
+ > **Note**: Dynamic client registration is enabled by default in HTTP mode. Use `--no-dynamic-registration` to disable it. If using a custom Azure Entra app, the platform type for your redirect URI depends on whether the app has a client secret: with a secret use "Web", without one use "Mobile and desktop applications" (never "Single-page application").
289
289
 
290
290
  **Quick test setup** using the default Azure app (ID `ms-365` and `localhost:8080` are pre-configured):
291
291
 
@@ -508,6 +508,14 @@ function findUsedSchemas(openApiSpec) {
508
508
  );
509
509
  schemasToProcess.push(schemaName);
510
510
  }
511
+ // Trace refs nested anywhere in an inline request body (e.g. a property's
512
+ // anyOf: [{$ref}, {nullable object}]). Without this they're pruned as unused,
513
+ // then stripped as a "broken reference", degrading the body to a bare object.
514
+ if (content.schema) {
515
+ findRefsInObject(content.schema, (ref) =>
516
+ schemasToProcess.push(ref.replace('#/components/schemas/', ''))
517
+ );
518
+ }
511
519
  });
512
520
  }
513
521
 
@@ -547,6 +555,12 @@ function findUsedSchemas(openApiSpec) {
547
555
  }
548
556
  });
549
557
  }
558
+ // Trace refs nested anywhere in an inline response schema.
559
+ if (content.schema) {
560
+ findRefsInObject(content.schema, (ref) =>
561
+ schemasToProcess.push(ref.replace('#/components/schemas/', ''))
562
+ );
563
+ }
550
564
  });
551
565
  }
552
566
  });
@@ -914,4 +914,118 @@ 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
+ });
917
1031
  });
@@ -825,6 +825,15 @@ const copy_drive_item_Body = z.object({
825
825
  childrenOnly: z.boolean().nullable().default(false),
826
826
  includeAllVersionHistory: z.boolean().nullable().default(false)
827
827
  }).partial().passthrough();
828
+ const microsoft_graph_driveRecipient = z.object({
829
+ alias: z.string().describe(
830
+ "The alias of the domain object, for cases where an email address is unavailable (for example, security groups)."
831
+ ).nullish(),
832
+ email: z.string().describe(
833
+ "The email address for the recipient, if the recipient has an associated email address."
834
+ ).nullish(),
835
+ objectId: z.string().describe("The unique identifier for the recipient in the directory.").nullish()
836
+ }).passthrough();
828
837
  const create_drive_item_share_link_Body = z.object({
829
838
  type: z.string().nullable(),
830
839
  scope: z.string().nullable(),
@@ -833,7 +842,7 @@ const create_drive_item_share_link_Body = z.object({
833
842
  ).datetime({ offset: true }).nullable(),
834
843
  password: z.string().nullable(),
835
844
  message: z.string().nullable(),
836
- recipients: z.array(z.object({}).partial().passthrough()),
845
+ recipients: z.array(microsoft_graph_driveRecipient),
837
846
  retainInheritedPermissions: z.boolean().nullable().default(false),
838
847
  sendNotification: z.boolean().nullable().default(false)
839
848
  }).partial().passthrough();
@@ -917,7 +926,51 @@ const microsoft_graph_permission = z.object({
917
926
  "A unique token that can be used to access this shared item via the shares API. Read-only."
918
927
  ).nullish()
919
928
  }).passthrough();
920
- const create_upload_session_Body = z.object({ item: z.object({}).partial().passthrough() }).partial().passthrough();
929
+ const microsoft_graph_driveItemSourceApplication = z.enum([
930
+ "teams",
931
+ "yammer",
932
+ "sharePoint",
933
+ "oneDrive",
934
+ "stream",
935
+ "powerPoint",
936
+ "office",
937
+ "loki",
938
+ "loop",
939
+ "other",
940
+ "unknownFutureValue"
941
+ ]);
942
+ const microsoft_graph_driveItemSource = z.object({
943
+ application: microsoft_graph_driveItemSourceApplication.optional(),
944
+ externalId: z.string().describe("The external identifier for the drive item from the source.").nullish()
945
+ }).passthrough();
946
+ const microsoft_graph_mediaSourceContentCategory = z.enum([
947
+ "meeting",
948
+ "liveStream",
949
+ "presentation",
950
+ "screenRecording",
951
+ "story",
952
+ "profile",
953
+ "chat",
954
+ "note",
955
+ "comment",
956
+ "unknownFutureValue"
957
+ ]);
958
+ const microsoft_graph_mediaSource = z.object({ contentCategory: microsoft_graph_mediaSourceContentCategory.optional() }).passthrough();
959
+ const microsoft_graph_driveItemUploadableProperties = z.object({
960
+ description: z.string().describe(
961
+ "Provides a user-visible description of the item. Read-write. Only on OneDrive Personal."
962
+ ).nullish(),
963
+ driveItemSource: microsoft_graph_driveItemSource.optional(),
964
+ fileSize: z.number().describe(
965
+ "Provides an expected file size to perform a quota check before uploading. Only on OneDrive Personal."
966
+ ).nullish(),
967
+ fileSystemInfo: microsoft_graph_fileSystemInfo.optional(),
968
+ mediaSource: microsoft_graph_mediaSource.optional(),
969
+ name: z.string().describe("The name of the item (filename and extension). Read-write.").nullish()
970
+ }).passthrough();
971
+ const create_upload_session_Body = z.object({
972
+ item: z.union([microsoft_graph_driveItemUploadableProperties, z.object({}).partial().passthrough()])
973
+ }).partial().passthrough();
921
974
  const microsoft_graph_uploadSession = z.object({
922
975
  expirationDateTime: z.string().regex(
923
976
  /^[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])$/
@@ -951,7 +1004,7 @@ const share_drive_item_Body = z.object({
951
1004
  roles: z.array(z.string().nullable()),
952
1005
  sendInvitation: z.boolean().nullable().default(false),
953
1006
  message: z.string().nullable(),
954
- recipients: z.array(z.object({}).partial().passthrough()),
1007
+ recipients: z.array(microsoft_graph_driveRecipient),
955
1008
  retainInheritedPermissions: z.boolean().nullable().default(false),
956
1009
  expirationDateTime: z.string().nullable(),
957
1010
  password: z.string().nullable()
@@ -3182,6 +3235,20 @@ const microsoft_graph_attachmentCollectionResponse = z.object({
3182
3235
  "@odata.nextLink": z.string().nullable(),
3183
3236
  value: z.array(microsoft_graph_attachment)
3184
3237
  }).partial().passthrough();
3238
+ const microsoft_graph_attachmentType = z.enum(["file", "item", "reference"]);
3239
+ const microsoft_graph_attachmentItem = z.object({
3240
+ attachmentType: microsoft_graph_attachmentType.optional(),
3241
+ contentId: z.string().describe(
3242
+ "The CID or Content-Id of the attachment for referencing for the in-line attachments using the <img src='cid:contentId'> tag in HTML messages. Optional."
3243
+ ).nullish(),
3244
+ contentType: z.string().describe("The nature of the data in the attachment. Optional.").nullish(),
3245
+ isInline: z.boolean().describe("true if the attachment is an inline attachment; otherwise, false. Optional.").nullish(),
3246
+ name: z.string().describe(
3247
+ "The display name of the attachment. This can be a descriptive string and doesn't have to be the actual file name. Required."
3248
+ ).nullish(),
3249
+ size: z.number().describe("The length of the attachment in bytes. Required.").nullish()
3250
+ }).passthrough();
3251
+ const create_mail_attachment_upload_session_Body = z.object({ AttachmentItem: microsoft_graph_attachmentItem }).partial().passthrough();
3185
3252
  const create_forward_draft_Body = z.object({
3186
3253
  ToRecipients: z.array(microsoft_graph_recipient),
3187
3254
  Message: z.union([microsoft_graph_message, z.object({}).partial().passthrough()]),
@@ -3875,13 +3942,27 @@ const microsoft_graph_userScopeTeamsAppInstallationCollectionResponse = z.object
3875
3942
  "@odata.nextLink": z.string().nullable(),
3876
3943
  value: z.array(microsoft_graph_userScopeTeamsAppInstallation)
3877
3944
  }).partial().passthrough();
3945
+ const microsoft_graph_teamworkActivityTopicSource = z.enum(["entityUrl", "text"]);
3946
+ const microsoft_graph_teamworkActivityTopic = z.object({
3947
+ source: microsoft_graph_teamworkActivityTopicSource.optional(),
3948
+ value: z.string().describe(
3949
+ "The topic value. If the value of the source property is entityUrl, this must be a Microsoft Graph URL. If the value is text, this must be a plain text value."
3950
+ ).optional(),
3951
+ webUrl: z.string().describe(
3952
+ "The link the user clicks when they select the notification. Optional when source is entityUrl; required when source is text."
3953
+ ).nullish()
3954
+ }).passthrough();
3955
+ const microsoft_graph_keyValuePair = z.object({
3956
+ name: z.string().describe("Name for this key-value pair").optional(),
3957
+ value: z.string().describe("Value for this key-value pair").nullish()
3958
+ }).passthrough();
3878
3959
  const send_my_activity_notification_Body = z.object({
3879
- topic: z.object({}).partial().passthrough(),
3960
+ topic: z.union([microsoft_graph_teamworkActivityTopic, z.object({}).partial().passthrough()]),
3880
3961
  activityType: z.string().nullable(),
3881
3962
  chainId: z.number().nullable(),
3882
3963
  previewText: z.union([microsoft_graph_itemBody, z.object({}).partial().passthrough()]),
3883
3964
  teamsAppId: z.string().nullable(),
3884
- templateParameters: z.array(z.object({}).partial().passthrough()),
3965
+ templateParameters: z.array(microsoft_graph_keyValuePair),
3885
3966
  iconId: z.string().nullable()
3886
3967
  }).partial().passthrough();
3887
3968
  const microsoft_graph_wellknownListName = z.enum([
@@ -4762,6 +4843,7 @@ const schemas = {
4762
4843
  microsoft_graph_driveItem,
4763
4844
  microsoft_graph_driveItemCollectionResponse,
4764
4845
  copy_drive_item_Body,
4846
+ microsoft_graph_driveRecipient,
4765
4847
  create_drive_item_share_link_Body,
4766
4848
  microsoft_graph_sharePointGroupIdentity,
4767
4849
  microsoft_graph_sharePointIdentity,
@@ -4769,6 +4851,11 @@ const schemas = {
4769
4851
  microsoft_graph_sharingInvitation,
4770
4852
  microsoft_graph_sharingLink,
4771
4853
  microsoft_graph_permission,
4854
+ microsoft_graph_driveItemSourceApplication,
4855
+ microsoft_graph_driveItemSource,
4856
+ microsoft_graph_mediaSourceContentCategory,
4857
+ microsoft_graph_mediaSource,
4858
+ microsoft_graph_driveItemUploadableProperties,
4772
4859
  create_upload_session_Body,
4773
4860
  microsoft_graph_uploadSession,
4774
4861
  BaseDeltaFunctionResponse,
@@ -4993,6 +5080,9 @@ const schemas = {
4993
5080
  microsoft_graph_messageRuleCollectionResponse,
4994
5081
  microsoft_graph_messageCollectionResponse,
4995
5082
  microsoft_graph_attachmentCollectionResponse,
5083
+ microsoft_graph_attachmentType,
5084
+ microsoft_graph_attachmentItem,
5085
+ create_mail_attachment_upload_session_Body,
4996
5086
  create_forward_draft_Body,
4997
5087
  create_reply_draft_Body,
4998
5088
  microsoft_graph_attendeeBase,
@@ -5070,6 +5160,9 @@ const schemas = {
5070
5160
  microsoft_graph_associatedTeamInfoCollectionResponse,
5071
5161
  microsoft_graph_userScopeTeamsAppInstallation,
5072
5162
  microsoft_graph_userScopeTeamsAppInstallationCollectionResponse,
5163
+ microsoft_graph_teamworkActivityTopicSource,
5164
+ microsoft_graph_teamworkActivityTopic,
5165
+ microsoft_graph_keyValuePair,
5073
5166
  send_my_activity_notification_Body,
5074
5167
  microsoft_graph_wellknownListName,
5075
5168
  microsoft_graph_taskStatus,
@@ -10176,7 +10269,7 @@ resource.`,
10176
10269
  name: "body",
10177
10270
  description: `Action parameters`,
10178
10271
  type: "Body",
10179
- schema: z.object({ AttachmentItem: z.object({}).partial().passthrough() }).partial().passthrough()
10272
+ schema: create_mail_attachment_upload_session_Body
10180
10273
  }
10181
10274
  ],
10182
10275
  response: z.void()
@@ -169,6 +169,93 @@ 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
+ }
172
259
  }
173
260
  ];
174
261
  function registerUtilityToolWithMcp(server, utility, ctx) {
@@ -127,7 +127,7 @@ When deploying for an organization, create a dedicated app registration instead
127
127
  - Claude (claude.ai, Desktop, Cowork): `https://claude.ai/api/mcp/auth_callback`
128
128
  - Other clients: check the `redirect_uri` query parameter your client sends to the server's `/authorize` endpoint (visible in the server logs)
129
129
 
130
- > **Common pitfall**: registering `https://your-server-domain/callback` here breaks sign-in with `AADSTS50011` (redirect URI mismatch) after the user authenticates. The server has no callback endpoint of its own; the authorization code always goes to the MCP client.
130
+ > **Common pitfall**: registering `https://your-server-domain/callback` here breaks sign-in with `AADSTS50011` (redirect URI mismatch) after the user authenticates. The server has no callback endpoint of its own; the authorization code always goes to the MCP client. Note that platform type **Web** applies because this setup uses a client secret; an app without a secret must register the redirect URI under "Mobile and desktop applications" instead.
131
131
 
132
132
  2. **Add API permissions** > Microsoft Graph > Delegated permissions
133
133
  Run `npx @softeria/ms-365-mcp-server --org-mode --list-permissions` to print the exact list of permissions required for your enabled tools.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@softeria/ms-365-mcp-server",
3
- "version": "0.119.0",
3
+ "version": "0.120.1",
4
4
  "description": " A Model Context Protocol (MCP) server for interacting with Microsoft 365 and Office services through the Graph API",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",