@softeria/ms-365-mcp-server 0.154.3 → 0.156.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 +24 -1
- package/dist/cli.js +6 -0
- package/dist/endpoints.json +33 -0
- package/dist/generated/client-beta.js +101 -1
- package/dist/graph-tools.js +2 -2
- package/dist/lib/param-descriptions.js +3 -2
- package/dist/lib/query-parameter-schema.js +8 -1
- package/dist/lib/tool-schema.js +1 -1
- package/dist/mcp-instructions.js +1 -1
- package/dist/server.js +66 -3
- package/package.json +1 -1
- package/src/endpoints.json +33 -0
package/README.md
CHANGED
|
@@ -105,6 +105,20 @@ Email (Outlook), Calendar, OneDrive Files, Excel, OneNote, To Do Tasks, Planner,
|
|
|
105
105
|
|
|
106
106
|
Teams & Chats, Online Meetings, Transcripts & Recordings, Attendance Reports, SharePoint Sites & Lists, Shared Mailboxes & Calendars, User Management, Presence, Virtual Events
|
|
107
107
|
|
|
108
|
+
Custom Teams emojis are available in organization mode through `list-custom-emojis`
|
|
109
|
+
and `create-custom-emoji` (`teams` and `work` presets). These use the Microsoft Graph
|
|
110
|
+
beta API and request the delegated permissions `TeamworkCustomEmoji.Read` and
|
|
111
|
+
`TeamworkCustomEmoji.Create`, respectively. Read-only mode exposes only the list tool.
|
|
112
|
+
Existing deployments may need consent for these new scopes and reauthentication;
|
|
113
|
+
adding tool support does not upgrade an already-issued token.
|
|
114
|
+
|
|
115
|
+
Listing returns base64 image content, so use a small `top` and `filter` to keep
|
|
116
|
+
responses manageable. To create an emoji, pass `body: { displayName, contentBytes }`
|
|
117
|
+
with the exact approved name and base64 PNG/GIF file bytes. See Microsoft's
|
|
118
|
+
[list](https://learn.microsoft.com/en-us/graph/api/teamworkmessaging-list-customemojis?view=graph-rest-beta)
|
|
119
|
+
and [create](https://learn.microsoft.com/en-us/graph/api/teamworkmessaging-post-customemojis?view=graph-rest-beta)
|
|
120
|
+
contracts. These tools do not post messages or reactions.
|
|
121
|
+
|
|
108
122
|
### Required Graph API Permissions
|
|
109
123
|
|
|
110
124
|
Permissions are requested dynamically based on which tools are enabled. Use `--list-permissions` to see the exact permissions for your configuration:
|
|
@@ -596,7 +610,15 @@ When running as an MCP server, the following options can be used:
|
|
|
596
610
|
-v Enable verbose logging
|
|
597
611
|
--read-only Start server in read-only mode, disabling write operations
|
|
598
612
|
--http [port] Use Streamable HTTP transport instead of stdio (optionally specify port, default: 3000)
|
|
599
|
-
Starts Express.js server with MCP endpoint at /mcp
|
|
613
|
+
Starts Express.js server with MCP endpoint at /mcp. Bound to a loopback host
|
|
614
|
+
(e.g. --http 127.0.0.1:3000 or --http [::1]:3000) with no --public-url, it
|
|
615
|
+
rejects requests whose Host or Origin is not localhost (not applied to the
|
|
616
|
+
--attachment-port listener)
|
|
617
|
+
--http-local-file-tools Register download-bytes-to-file over HTTP. Anyone who can reach the port
|
|
618
|
+
can write files as the server's user (without a valid token, only an empty
|
|
619
|
+
file that is removed again), so enable it only on a single-user machine.
|
|
620
|
+
Refused unless --http binds a loopback host with no --public-url and no
|
|
621
|
+
--trust-proxy-auth
|
|
600
622
|
--enable-auth-tools Enable login/logout tools when using HTTP mode (disabled by default in HTTP mode)
|
|
601
623
|
--enable-attachment-urls Let get-download-url mint a server-served URL for byte resources Graph
|
|
602
624
|
exposes no pre-authenticated URL for (see "Server-Minted Attachment URLs")
|
|
@@ -634,6 +656,7 @@ Environment variables:
|
|
|
634
656
|
- `MS365_MCP_TRUST_PROXY_HOPS=<n>`: Number of trusted reverse-proxy hops in HTTP mode (default `1`). Accurate per-IP rate limiting depends on this matching your deployment — set to the number of proxies in front of the server, `0` to use the raw socket peer IP, or a comma-separated subnet list
|
|
635
657
|
- `MS365_MCP_ATTACHMENT_PORT=<port>`: Serve the attachment route on its own listener on this port (alternative to --attachment-port; requires `--enable-attachment-urls`)
|
|
636
658
|
- `MS365_MCP_ATTACHMENT_HOST=<host>`: Interface the `MS365_MCP_ATTACHMENT_PORT` listener binds (alternative to --attachment-host; requires `--attachment-port`). Defaults to the host `--http` bound — which for a wildcard `--http` means both ports answer everywhere and the port split isolates nothing. See "Splitting the attachment listener"
|
|
659
|
+
- `MS365_MCP_HTTP_LOCAL_FILE_TOOLS=true|1`: Register download-bytes-to-file over HTTP (alternative to --http-local-file-tools; same restrictions)
|
|
637
660
|
- `MS365_MCP_CLOUD_TYPE=global|china`: Microsoft cloud environment (alternative to --cloud flag)
|
|
638
661
|
- `LOG_LEVEL`: Set logging level (default: 'info')
|
|
639
662
|
- `SILENT=true|1`: Disable console output
|
package/dist/cli.js
CHANGED
|
@@ -69,6 +69,9 @@ program.name("ms-365-mcp-server").description("Microsoft 365 MCP Server").versio
|
|
|
69
69
|
).option(
|
|
70
70
|
"--trust-proxy-auth",
|
|
71
71
|
"In HTTP mode, skip the built-in Bearer-token check on /mcp and ignore any forwarded Authorization header. All callers share the locally cached MSAL identity (same path stdio mode uses). Use only when an upstream reverse proxy has already authenticated the caller."
|
|
72
|
+
).option(
|
|
73
|
+
"--http-local-file-tools",
|
|
74
|
+
"In HTTP mode, also register download-bytes-to-file, which writes files on the server as the server's user. Anyone who can reach the port can use it, so enable only on a single-user machine. Refused unless --http binds a loopback host with no --public-url and no --trust-proxy-auth."
|
|
72
75
|
).option(
|
|
73
76
|
"--allow-unauthenticated-discovery",
|
|
74
77
|
"In HTTP mode, allow MCP discovery requests (initialize, tools/list, prompts/list, resources/list, ping) without a bearer token, so a gateway can enumerate the tool catalog before any user has authenticated. Non-discovery requests (e.g. tools/call) still require a token. Off by default."
|
|
@@ -204,6 +207,9 @@ function parseArgs() {
|
|
|
204
207
|
if (process.env.MS365_MCP_TRUST_PROXY_AUTH === "true" || process.env.MS365_MCP_TRUST_PROXY_AUTH === "1") {
|
|
205
208
|
options.trustProxyAuth = true;
|
|
206
209
|
}
|
|
210
|
+
if (process.env.MS365_MCP_HTTP_LOCAL_FILE_TOOLS === "true" || process.env.MS365_MCP_HTTP_LOCAL_FILE_TOOLS === "1") {
|
|
211
|
+
options.httpLocalFileTools = true;
|
|
212
|
+
}
|
|
207
213
|
if (process.env.MS365_MCP_ALLOW_UNAUTHENTICATED_DISCOVERY === "true" || process.env.MS365_MCP_ALLOW_UNAUTHENTICATED_DISCOVERY === "1") {
|
|
208
214
|
options.allowUnauthenticatedDiscovery = true;
|
|
209
215
|
}
|
package/dist/endpoints.json
CHANGED
|
@@ -1659,6 +1659,39 @@
|
|
|
1659
1659
|
"presets": ["teams", "teams-write", "work"],
|
|
1660
1660
|
"workScopes": ["Team.ReadBasic.All"]
|
|
1661
1661
|
},
|
|
1662
|
+
{
|
|
1663
|
+
"pathPattern": "/teamwork/messaging/customEmojis",
|
|
1664
|
+
"method": "get",
|
|
1665
|
+
"toolName": "list-custom-emojis",
|
|
1666
|
+
"apiVersion": "beta",
|
|
1667
|
+
"presets": ["teams", "work"],
|
|
1668
|
+
"workScopes": ["TeamworkCustomEmoji.Read"],
|
|
1669
|
+
"llmTip": "Lists the organization's custom Teams emojis, including displayName, contentBytes (base64 PNG/GIF), createdBy and createdDateTime. The documented query options are $top and $filter. Image bytes can make responses large: start with a small $top and narrow with $filter; use fetchAllPages only for an explicitly requested export. displayName is the unique key, not an id. Work/school accounts only. Microsoft Graph beta API: subject to change."
|
|
1670
|
+
},
|
|
1671
|
+
{
|
|
1672
|
+
"pathPattern": "/teamwork/messaging/customEmojis",
|
|
1673
|
+
"method": "post",
|
|
1674
|
+
"toolName": "create-custom-emoji",
|
|
1675
|
+
"apiVersion": "beta",
|
|
1676
|
+
"presets": ["teams", "work"],
|
|
1677
|
+
"workScopes": ["TeamworkCustomEmoji.Create"],
|
|
1678
|
+
"requestBodySchema": {
|
|
1679
|
+
"type": "object",
|
|
1680
|
+
"required": ["displayName", "contentBytes"],
|
|
1681
|
+
"properties": {
|
|
1682
|
+
"displayName": {
|
|
1683
|
+
"type": "string",
|
|
1684
|
+
"description": "Exact unique custom emoji name, without surrounding colons. Must not conflict with an existing emoji name."
|
|
1685
|
+
},
|
|
1686
|
+
"contentBytes": {
|
|
1687
|
+
"type": "string",
|
|
1688
|
+
"description": "Base64-encoded PNG or GIF file content; do not include a data-URL prefix."
|
|
1689
|
+
}
|
|
1690
|
+
},
|
|
1691
|
+
"additionalProperties": false
|
|
1692
|
+
},
|
|
1693
|
+
"llmTip": "Uploads a custom Teams emoji for the organization using body: { displayName, contentBytes }. Only PNG and GIF are supported; pass the complete image file as base64. Confirm the exact name and image with the user before creating it. Returns the created emoji, including its image bytes; use excludeResponse=true when only success/failure is needed. Work/school accounts only. Microsoft Graph beta API: subject to change."
|
|
1694
|
+
},
|
|
1662
1695
|
{
|
|
1663
1696
|
"pathPattern": "/teams/{team-id}",
|
|
1664
1697
|
"method": "get",
|
|
@@ -742,6 +742,36 @@ const microsoft_graph_plannerTaskChatMessageCollectionResponse = z.object({
|
|
|
742
742
|
"@odata.nextLink": z.string().nullable(),
|
|
743
743
|
value: z.array(microsoft_graph_plannerTaskChatMessage)
|
|
744
744
|
}).partial().passthrough();
|
|
745
|
+
const microsoft_graph_customEmojiFromIdentitySet = z.object({
|
|
746
|
+
application: microsoft_graph_identity.optional(),
|
|
747
|
+
device: microsoft_graph_identity.optional(),
|
|
748
|
+
user: microsoft_graph_identity.optional()
|
|
749
|
+
}).passthrough();
|
|
750
|
+
const microsoft_graph_teamworkCustomEmoji = z.object({
|
|
751
|
+
contentBytes: z.string().describe(
|
|
752
|
+
"The base64-encoded image content of the emoji. Supported formats include PNG and GIF."
|
|
753
|
+
).nullish(),
|
|
754
|
+
createdBy: microsoft_graph_customEmojiFromIdentitySet.optional(),
|
|
755
|
+
createdDateTime: z.string().regex(
|
|
756
|
+
/^[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])$/
|
|
757
|
+
).datetime({ offset: true }).describe(
|
|
758
|
+
"The date and time when the emoji was created. The timestamp type represents date and time information using ISO 8601 format and is always in UTC. For example, midnight UTC on Jan 1, 2024, is 2024-01-01T00:00:00Z."
|
|
759
|
+
).optional(),
|
|
760
|
+
displayName: z.string().describe(
|
|
761
|
+
"The unique display name of the custom emoji. Key. Must be unique and must not conflict with existing emoji names."
|
|
762
|
+
).optional()
|
|
763
|
+
}).passthrough();
|
|
764
|
+
const microsoft_graph_teamworkCustomEmojiCollectionResponse = z.object({
|
|
765
|
+
"@odata.count": z.number().int().nullable(),
|
|
766
|
+
"@odata.nextLink": z.string().nullable(),
|
|
767
|
+
value: z.array(microsoft_graph_teamworkCustomEmoji)
|
|
768
|
+
}).partial().passthrough();
|
|
769
|
+
const create_custom_emoji_Body = z.object({
|
|
770
|
+
displayName: z.string().describe(
|
|
771
|
+
"Exact unique custom emoji name, without surrounding colons. Must not conflict with an existing emoji name."
|
|
772
|
+
),
|
|
773
|
+
contentBytes: z.string().describe("Base64-encoded PNG or GIF file content; do not include a data-URL prefix.")
|
|
774
|
+
}).passthrough();
|
|
745
775
|
const schemas = {
|
|
746
776
|
microsoft_graph_allowedAudiences,
|
|
747
777
|
microsoft_graph_identity,
|
|
@@ -800,7 +830,11 @@ const schemas = {
|
|
|
800
830
|
microsoft_graph_plannerTaskChatReactionEvent,
|
|
801
831
|
microsoft_graph_plannerTaskChatReaction,
|
|
802
832
|
microsoft_graph_plannerTaskChatMessage,
|
|
803
|
-
microsoft_graph_plannerTaskChatMessageCollectionResponse
|
|
833
|
+
microsoft_graph_plannerTaskChatMessageCollectionResponse,
|
|
834
|
+
microsoft_graph_customEmojiFromIdentitySet,
|
|
835
|
+
microsoft_graph_teamworkCustomEmoji,
|
|
836
|
+
microsoft_graph_teamworkCustomEmojiCollectionResponse,
|
|
837
|
+
create_custom_emoji_Body
|
|
804
838
|
};
|
|
805
839
|
const endpoints = makeApi([
|
|
806
840
|
{
|
|
@@ -903,6 +937,72 @@ const endpoints = makeApi([
|
|
|
903
937
|
}
|
|
904
938
|
],
|
|
905
939
|
response: z.void()
|
|
940
|
+
},
|
|
941
|
+
{
|
|
942
|
+
method: "get",
|
|
943
|
+
path: "/teamwork/messaging/customEmojis",
|
|
944
|
+
alias: "list-custom-emojis",
|
|
945
|
+
description: `Get a list of custom emojis available in the teamwork messaging of the organization.`,
|
|
946
|
+
requestFormat: "json",
|
|
947
|
+
parameters: [
|
|
948
|
+
{
|
|
949
|
+
name: "$top",
|
|
950
|
+
type: "Query",
|
|
951
|
+
schema: z.number().int().gte(0).describe("Show only the first n items").optional()
|
|
952
|
+
},
|
|
953
|
+
{
|
|
954
|
+
name: "$skip",
|
|
955
|
+
type: "Query",
|
|
956
|
+
schema: z.number().int().gte(0).describe("Skip the first n items").optional()
|
|
957
|
+
},
|
|
958
|
+
{
|
|
959
|
+
name: "$search",
|
|
960
|
+
type: "Query",
|
|
961
|
+
schema: z.string().describe("Search items by search phrases").optional()
|
|
962
|
+
},
|
|
963
|
+
{
|
|
964
|
+
name: "$filter",
|
|
965
|
+
type: "Query",
|
|
966
|
+
schema: z.string().describe("Filter items by property values").optional()
|
|
967
|
+
},
|
|
968
|
+
{
|
|
969
|
+
name: "$count",
|
|
970
|
+
type: "Query",
|
|
971
|
+
schema: z.boolean().describe("Include count of items").optional()
|
|
972
|
+
},
|
|
973
|
+
{
|
|
974
|
+
name: "$orderby",
|
|
975
|
+
type: "Query",
|
|
976
|
+
schema: z.array(z.string()).describe("Order items by property values").optional()
|
|
977
|
+
},
|
|
978
|
+
{
|
|
979
|
+
name: "$select",
|
|
980
|
+
type: "Query",
|
|
981
|
+
schema: z.array(z.string()).describe("Select properties to be returned").optional()
|
|
982
|
+
},
|
|
983
|
+
{
|
|
984
|
+
name: "$expand",
|
|
985
|
+
type: "Query",
|
|
986
|
+
schema: z.array(z.string()).describe("Expand related entities").optional()
|
|
987
|
+
}
|
|
988
|
+
],
|
|
989
|
+
response: microsoft_graph_teamworkCustomEmojiCollectionResponse
|
|
990
|
+
},
|
|
991
|
+
{
|
|
992
|
+
method: "post",
|
|
993
|
+
path: "/teamwork/messaging/customEmojis",
|
|
994
|
+
alias: "create-custom-emoji",
|
|
995
|
+
description: `Create a new custom emoji in the teamwork messaging of the organization, which adds the custom emoji to Teams for the tenant. The emoji image is provided as base64-encoded content bytes.`,
|
|
996
|
+
requestFormat: "json",
|
|
997
|
+
parameters: [
|
|
998
|
+
{
|
|
999
|
+
name: "body",
|
|
1000
|
+
description: `New navigation property`,
|
|
1001
|
+
type: "Body",
|
|
1002
|
+
schema: create_custom_emoji_Body
|
|
1003
|
+
}
|
|
1004
|
+
],
|
|
1005
|
+
response: microsoft_graph_teamworkCustomEmoji
|
|
906
1006
|
}
|
|
907
1007
|
]);
|
|
908
1008
|
const api = new Zodios(endpoints);
|
package/dist/graph-tools.js
CHANGED
|
@@ -699,7 +699,7 @@ const UTILITY_TOOLS = [
|
|
|
699
699
|
// description at ~40 tokens, so the OneDrive/SharePoint guidance below
|
|
700
700
|
// sits past the cap. That keeps the hint for the reading LLM while letting
|
|
701
701
|
// get-download-url own the high-signal "drive"/"sharepoint" search terms.
|
|
702
|
-
description: "Write authenticated Microsoft Graph byte content to a local file on the server, returning { path, contentType, bytesWritten } instead of base64. The only out-of-band way to save mail attachments and meeting recordings, whose bytes are exposed solely through authenticated endpoints. Also handles profile photos and Teams hosted content. Writes to an absolute outputPath and never overwrites an existing file. stdio mode
|
|
702
|
+
description: "Write authenticated Microsoft Graph byte content to a local file on the server, returning { path, contentType, bytesWritten } instead of base64. The only out-of-band way to save mail attachments and meeting recordings, whose bytes are exposed solely through authenticated endpoints. Also handles profile photos and Teams hosted content. Writes to an absolute outputPath and never overwrites an existing file. stdio mode, or HTTP with --http-local-file-tools. For OneDrive or SharePoint file content, get-download-url is preferred \u2014 it returns a pre-authenticated URL for fully out-of-band download without the server fetching the bytes.",
|
|
703
703
|
readOnlyHint: true,
|
|
704
704
|
openWorldHint: true,
|
|
705
705
|
stdioOnly: true,
|
|
@@ -1671,7 +1671,7 @@ function registerGraphTools(server, graphClient, readOnly = false, enabledToolsP
|
|
|
1671
1671
|
}
|
|
1672
1672
|
if (isFetchAllPagesApplicable(tool)) {
|
|
1673
1673
|
const maxPages = getMaxPages();
|
|
1674
|
-
paramSchema["fetchAllPages"] = z.boolean().describe(getFetchAllPagesParamDescription(maxPages)).optional();
|
|
1674
|
+
paramSchema["fetchAllPages"] = z.boolean().describe(getFetchAllPagesParamDescription(maxPages, tool.alias)).optional();
|
|
1675
1675
|
}
|
|
1676
1676
|
if (isSkiptokenApplicable(tool, Object.keys(paramSchema))) {
|
|
1677
1677
|
paramSchema["skiptoken"] = z.string().describe(SKIPTOKEN_PARAM_DESCRIPTION).optional();
|
|
@@ -55,8 +55,9 @@ function getAccountParamDescription(accountNames) {
|
|
|
55
55
|
const accountHint = accountNames.length > 0 ? `Known accounts: ${accountNames.join(", ")}. ` : "";
|
|
56
56
|
return `${accountHint}Microsoft account email to use for this request. Required when multiple accounts are configured. Use the list-accounts tool to discover all currently available accounts.`;
|
|
57
57
|
}
|
|
58
|
-
function getFetchAllPagesParamDescription(maxPages) {
|
|
59
|
-
|
|
58
|
+
function getFetchAllPagesParamDescription(maxPages, toolAlias) {
|
|
59
|
+
const narrowingOptions = toolAlias === "list-custom-emojis" ? "$filter" : "$filter/$search";
|
|
60
|
+
return `Follow @odata.nextLink and merge up to ${maxPages} pages into one response. Can return enormous payloads\u2014only when the user explicitly needs a full export. Prefer a small $top first, then paginate or narrow with ${narrowingOptions}.`;
|
|
60
61
|
}
|
|
61
62
|
function getODataParamDescription(bareName) {
|
|
62
63
|
switch (bareName) {
|
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { getODataParamDescription, shouldOmitTopParam } from "./param-descriptions.js";
|
|
3
3
|
const TEAM_LIST_TOOLS = /* @__PURE__ */ new Set(["list-joined-teams", "list-my-associated-teams"]);
|
|
4
|
+
const CUSTOM_EMOJI_QUERY_DESCRIPTIONS = {
|
|
5
|
+
top: "Number of custom emojis to return in one page. Each emoji includes base64 image content; use a small page size to keep the response manageable.",
|
|
6
|
+
filter: "OData filter expression for custom emojis, forwarded to Microsoft Graph. Filter support is determined by the beta API."
|
|
7
|
+
};
|
|
4
8
|
function queryParameterSchema(toolName, name, providerSchema) {
|
|
5
9
|
const bareName = name.replace(/^\$/, "").toLowerCase();
|
|
6
10
|
if (TEAM_LIST_TOOLS.has(toolName)) return void 0;
|
|
11
|
+
if (toolName === "list-custom-emojis" && !["top", "filter", "skiptoken"].includes(bareName)) {
|
|
12
|
+
return void 0;
|
|
13
|
+
}
|
|
7
14
|
if (bareName === "top" && shouldOmitTopParam(toolName)) return void 0;
|
|
8
15
|
const source = providerSchema instanceof z.ZodOptional ? providerSchema.unwrap() : providerSchema;
|
|
9
16
|
let schema = source;
|
|
@@ -25,7 +32,7 @@ function queryParameterSchema(toolName, name, providerSchema) {
|
|
|
25
32
|
break;
|
|
26
33
|
}
|
|
27
34
|
if (providerSchema.isOptional()) schema = schema.optional();
|
|
28
|
-
const description = getODataParamDescription(bareName);
|
|
35
|
+
const description = toolName === "list-custom-emojis" && CUSTOM_EMOJI_QUERY_DESCRIPTIONS[bareName] || getODataParamDescription(bareName);
|
|
29
36
|
return description ? schema.describe(description) : schema;
|
|
30
37
|
}
|
|
31
38
|
export {
|
package/dist/lib/tool-schema.js
CHANGED
|
@@ -53,7 +53,7 @@ function describeToolSchema(tool, config, ctx = {}) {
|
|
|
53
53
|
name: "fetchAllPages",
|
|
54
54
|
in: "Query",
|
|
55
55
|
required: false,
|
|
56
|
-
description: getFetchAllPagesParamDescription(getMaxPages()),
|
|
56
|
+
description: getFetchAllPagesParamDescription(getMaxPages(), tool.alias),
|
|
57
57
|
schema: { type: "boolean" }
|
|
58
58
|
});
|
|
59
59
|
}
|
package/dist/mcp-instructions.js
CHANGED
|
@@ -6,7 +6,7 @@ function buildGeneralMcpInstructions(opts) {
|
|
|
6
6
|
"When you need an organizational user or recipient address, resolve it with list-users (or another directory tool); do not invent SMTP addresses.",
|
|
7
7
|
"Directory $search on collections such as /users or /groups requires ConsistencyLevel: eventual when the tool exposes that header.",
|
|
8
8
|
"Teams chat and channel messages: prefer HTML contentType in the body; plain text is often mangled by Graph.",
|
|
9
|
-
"Files / binary content: for large drive/SharePoint file content, prefer get-download-url to resolve a pre-authenticated URL for out-of-band download. Use download-bytes for authenticated byte reads such as mail attachments, profile photos, Teams hosted content, and meeting recordings. In stdio mode, download-bytes-to-file writes those same authenticated bytes straight to a local absolute path instead of returning base64 \u2014 the only out-of-band option for large mail attachments and meeting recordings, which get-download-url cannot handle. These tools take relative Microsoft Graph paths, not absolute URLs. For uploads, upload-file-content takes a base64 string body (Graph allows 250MB, but the whole string passes through the agent context and a truncated one is written without error); use create-upload-session for anything but small files."
|
|
9
|
+
"Files / binary content: for large drive/SharePoint file content, prefer get-download-url to resolve a pre-authenticated URL for out-of-band download. Use download-bytes for authenticated byte reads such as mail attachments, profile photos, Teams hosted content, and meeting recordings. In stdio mode (or HTTP with --http-local-file-tools), download-bytes-to-file writes those same authenticated bytes straight to a local absolute path instead of returning base64 \u2014 the only out-of-band option for large mail attachments and meeting recordings, which get-download-url cannot handle. These tools take relative Microsoft Graph paths, not absolute URLs. For uploads, upload-file-content takes a base64 string body (Graph allows 250MB, but the whole string passes through the agent context and a truncated one is written without error); use create-upload-session for anything but small files."
|
|
10
10
|
];
|
|
11
11
|
if (opts.readOnly) parts.push("This server is read-only; write operations are disabled.");
|
|
12
12
|
if (opts.multiAccount)
|
package/dist/server.js
CHANGED
|
@@ -36,11 +36,40 @@ import { dumpError } from "./crash-logging.js";
|
|
|
36
36
|
import crypto from "node:crypto";
|
|
37
37
|
import { isIP, isIPv6 } from "node:net";
|
|
38
38
|
import OboClient from "./obo-client.js";
|
|
39
|
+
import { hostHeaderValidation } from "@modelcontextprotocol/sdk/server/middleware/hostHeaderValidation.js";
|
|
40
|
+
const LOOPBACK_HOSTNAMES = ["localhost", "127.0.0.1", "[::1]"];
|
|
41
|
+
function isLoopbackHost(host) {
|
|
42
|
+
if (!host) return false;
|
|
43
|
+
const bare = host.toLowerCase().replace(/^\[(.*)\]$/, "$1");
|
|
44
|
+
return bare === "localhost" || bare === "127.0.0.1" || bare === "::1";
|
|
45
|
+
}
|
|
46
|
+
function loopbackOriginValidation() {
|
|
47
|
+
return (req, res, next) => {
|
|
48
|
+
const origin = req.headers.origin;
|
|
49
|
+
if (origin === void 0) return next();
|
|
50
|
+
let hostname;
|
|
51
|
+
try {
|
|
52
|
+
hostname = new URL(origin).hostname;
|
|
53
|
+
} catch {
|
|
54
|
+
hostname = void 0;
|
|
55
|
+
}
|
|
56
|
+
if (hostname && LOOPBACK_HOSTNAMES.includes(hostname)) return next();
|
|
57
|
+
res.status(403).json({
|
|
58
|
+
jsonrpc: "2.0",
|
|
59
|
+
error: { code: -32e3, message: `Invalid Origin: ${origin}` },
|
|
60
|
+
id: null
|
|
61
|
+
});
|
|
62
|
+
};
|
|
63
|
+
}
|
|
39
64
|
function parseHttpOption(httpOption) {
|
|
40
65
|
if (typeof httpOption === "boolean") {
|
|
41
66
|
return { host: void 0, port: 3e3 };
|
|
42
67
|
}
|
|
43
68
|
const httpString = httpOption.trim();
|
|
69
|
+
const bracketed = /^\[([^\]]+)\](?::(.*))?$/.exec(httpString);
|
|
70
|
+
if (bracketed) {
|
|
71
|
+
return { host: bracketed[1], port: parseInt(bracketed[2] ?? "") || 3e3 };
|
|
72
|
+
}
|
|
44
73
|
if (httpString.includes(":")) {
|
|
45
74
|
const [hostPart, portPart] = httpString.split(":");
|
|
46
75
|
const host = hostPart || void 0;
|
|
@@ -131,6 +160,14 @@ class MicrosoftGraphServer {
|
|
|
131
160
|
this.secrets = null;
|
|
132
161
|
this.oboClient = null;
|
|
133
162
|
}
|
|
163
|
+
isLoopbackOnlyHttp() {
|
|
164
|
+
if (!this.options.http) return false;
|
|
165
|
+
const publicUrl = this.options.publicUrl || process.env.MS365_MCP_PUBLIC_URL || this.options.baseUrl || process.env.MS365_MCP_BASE_URL;
|
|
166
|
+
return !publicUrl && isLoopbackHost(parseHttpOption(this.options.http).host);
|
|
167
|
+
}
|
|
168
|
+
hidesStdioOnlyTools() {
|
|
169
|
+
return Boolean(this.options.http) && !this.options.httpLocalFileTools;
|
|
170
|
+
}
|
|
134
171
|
createMcpServer() {
|
|
135
172
|
const server = new McpServer(
|
|
136
173
|
{
|
|
@@ -161,7 +198,7 @@ class MicrosoftGraphServer {
|
|
|
161
198
|
this.accountNames,
|
|
162
199
|
this.options.enabledTools,
|
|
163
200
|
this.options.allowedScopes,
|
|
164
|
-
|
|
201
|
+
this.hidesStdioOnlyTools()
|
|
165
202
|
);
|
|
166
203
|
} else {
|
|
167
204
|
registerGraphTools(
|
|
@@ -174,7 +211,7 @@ class MicrosoftGraphServer {
|
|
|
174
211
|
this.multiAccount,
|
|
175
212
|
this.accountNames,
|
|
176
213
|
this.options.allowedScopes,
|
|
177
|
-
|
|
214
|
+
this.hidesStdioOnlyTools()
|
|
178
215
|
);
|
|
179
216
|
}
|
|
180
217
|
installToolSchemaRefNormalization(server);
|
|
@@ -202,6 +239,25 @@ class MicrosoftGraphServer {
|
|
|
202
239
|
'Account routing disabled: requests use the OAuth bearer identity, so the "account" parameter is not injected into tool schemas'
|
|
203
240
|
);
|
|
204
241
|
}
|
|
242
|
+
if (this.options.httpLocalFileTools && this.options.http) {
|
|
243
|
+
if (!this.isLoopbackOnlyHttp()) {
|
|
244
|
+
throw new Error(
|
|
245
|
+
"--http-local-file-tools requires --http bound to a loopback host (localhost, 127.0.0.1 or [::1]) and no --public-url: anyone who can reach the port can write files as the server's user."
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
if (this.options.trustProxyAuth) {
|
|
249
|
+
throw new Error(
|
|
250
|
+
"--http-local-file-tools cannot be combined with --trust-proxy-auth: a proxy in front of the loopback port would let remote callers write files as the server's user."
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
logger.warn(
|
|
254
|
+
"--http-local-file-tools: download-bytes-to-file is registered over HTTP; anyone who can reach this port can write files as the server's user."
|
|
255
|
+
);
|
|
256
|
+
} else if (this.options.httpLocalFileTools) {
|
|
257
|
+
logger.warn(
|
|
258
|
+
"--http-local-file-tools has no effect in stdio mode, where download-bytes-to-file is always registered."
|
|
259
|
+
);
|
|
260
|
+
}
|
|
205
261
|
if (this.options.obo) {
|
|
206
262
|
if (!this.options.http) {
|
|
207
263
|
throw new Error("--obo requires --http (On-Behalf-Of flow only works in HTTP mode).");
|
|
@@ -267,6 +323,10 @@ class MicrosoftGraphServer {
|
|
|
267
323
|
if (this.options.http) {
|
|
268
324
|
const { host, port } = parseHttpOption(this.options.http);
|
|
269
325
|
const app = express();
|
|
326
|
+
if (this.isLoopbackOnlyHttp()) {
|
|
327
|
+
app.use(hostHeaderValidation(LOOPBACK_HOSTNAMES));
|
|
328
|
+
app.use(loopbackOriginValidation());
|
|
329
|
+
}
|
|
270
330
|
const trustProxyEnv = process.env.MS365_MCP_TRUST_PROXY_HOPS;
|
|
271
331
|
if (trustProxyEnv !== void 0 && trustProxyEnv !== "") {
|
|
272
332
|
const asNum = Number(trustProxyEnv);
|
|
@@ -803,6 +863,9 @@ class MicrosoftGraphServer {
|
|
|
803
863
|
var server_default = MicrosoftGraphServer;
|
|
804
864
|
export {
|
|
805
865
|
server_default as default,
|
|
866
|
+
isLoopbackHost,
|
|
867
|
+
loopbackOriginValidation,
|
|
806
868
|
parseAttachmentHostOption,
|
|
807
|
-
parseAttachmentPortOption
|
|
869
|
+
parseAttachmentPortOption,
|
|
870
|
+
parseHttpOption
|
|
808
871
|
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@softeria/ms-365-mcp-server",
|
|
3
3
|
"mcpName": "io.github.Softeria/ms-365-mcp-server",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.156.0",
|
|
5
5
|
"description": " A Model Context Protocol (MCP) server for interacting with Microsoft 365 and Office services through the Graph API",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "dist/index.js",
|
package/src/endpoints.json
CHANGED
|
@@ -1659,6 +1659,39 @@
|
|
|
1659
1659
|
"presets": ["teams", "teams-write", "work"],
|
|
1660
1660
|
"workScopes": ["Team.ReadBasic.All"]
|
|
1661
1661
|
},
|
|
1662
|
+
{
|
|
1663
|
+
"pathPattern": "/teamwork/messaging/customEmojis",
|
|
1664
|
+
"method": "get",
|
|
1665
|
+
"toolName": "list-custom-emojis",
|
|
1666
|
+
"apiVersion": "beta",
|
|
1667
|
+
"presets": ["teams", "work"],
|
|
1668
|
+
"workScopes": ["TeamworkCustomEmoji.Read"],
|
|
1669
|
+
"llmTip": "Lists the organization's custom Teams emojis, including displayName, contentBytes (base64 PNG/GIF), createdBy and createdDateTime. The documented query options are $top and $filter. Image bytes can make responses large: start with a small $top and narrow with $filter; use fetchAllPages only for an explicitly requested export. displayName is the unique key, not an id. Work/school accounts only. Microsoft Graph beta API: subject to change."
|
|
1670
|
+
},
|
|
1671
|
+
{
|
|
1672
|
+
"pathPattern": "/teamwork/messaging/customEmojis",
|
|
1673
|
+
"method": "post",
|
|
1674
|
+
"toolName": "create-custom-emoji",
|
|
1675
|
+
"apiVersion": "beta",
|
|
1676
|
+
"presets": ["teams", "work"],
|
|
1677
|
+
"workScopes": ["TeamworkCustomEmoji.Create"],
|
|
1678
|
+
"requestBodySchema": {
|
|
1679
|
+
"type": "object",
|
|
1680
|
+
"required": ["displayName", "contentBytes"],
|
|
1681
|
+
"properties": {
|
|
1682
|
+
"displayName": {
|
|
1683
|
+
"type": "string",
|
|
1684
|
+
"description": "Exact unique custom emoji name, without surrounding colons. Must not conflict with an existing emoji name."
|
|
1685
|
+
},
|
|
1686
|
+
"contentBytes": {
|
|
1687
|
+
"type": "string",
|
|
1688
|
+
"description": "Base64-encoded PNG or GIF file content; do not include a data-URL prefix."
|
|
1689
|
+
}
|
|
1690
|
+
},
|
|
1691
|
+
"additionalProperties": false
|
|
1692
|
+
},
|
|
1693
|
+
"llmTip": "Uploads a custom Teams emoji for the organization using body: { displayName, contentBytes }. Only PNG and GIF are supported; pass the complete image file as base64. Confirm the exact name and image with the user before creating it. Returns the created emoji, including its image bytes; use excludeResponse=true when only success/failure is needed. Work/school accounts only. Microsoft Graph beta API: subject to change."
|
|
1694
|
+
},
|
|
1662
1695
|
{
|
|
1663
1696
|
"pathPattern": "/teams/{team-id}",
|
|
1664
1697
|
"method": "get",
|