@softeria/ms-365-mcp-server 0.167.0 β 0.169.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 +41 -0
- package/dist/__tests__/graph-tools.test.js +174 -7
- package/dist/endpoints.json +11 -8
- package/dist/graph-tools.js +214 -7
- package/dist/lib/mail-text.js +293 -0
- package/dist/lib/param-descriptions.js +23 -9
- package/dist/lib/query-parameter-schema.js +2 -7
- package/dist/lib/tool-schema.js +1 -1
- package/dist/tool-categories.js +2 -1
- package/package.json +1 -1
- package/src/endpoints.json +11 -8
package/README.md
CHANGED
|
@@ -736,6 +736,7 @@ Environment variables:
|
|
|
736
736
|
- `MS365_MCP_BLOB_MAX_BYTES=<n>`: With blob output on, the largest file a download tool reads (positive integer, default `5242880`). The server stops reading past it and answers an error naming the limit.
|
|
737
737
|
- `MS365_MCP_BODY_FORMAT=html`: Return email bodies as HTML instead of plain text (default: text)
|
|
738
738
|
- `MS365_MCP_OBO_ALLOWED_TENANTS=<ids>` / `MS365_MCP_OBO_ALLOWED_USERS=<ids>`: With `--obo`, only callers whose token `tid` (tenant id) / `oid` (user object id) is listed may use the server; others get 403 before any On-Behalf-Of exchange. Comma or space separated; unset = everyone. The claims are read without verifying the signature, which Entra verifies during the exchange, so a token that lies about them still gets no Graph token. A token Entra rejects at the exchange (bad signature, expired, wrong audience, consent needed) gets 401 with a `WWW-Authenticate` challenge so the client signs in again
|
|
739
|
+
- `MS365_MCP_READ_MAIL_TEXT_MAX_CHARS=<n>`: Largest result `read-mail-text` returns, in characters (default `80000`). A larger result is refused whole, naming the largest messages, rather than cut (see Reading Mail as Clean Text below)
|
|
739
740
|
- `MS365_MCP_MESSAGE_SIGNOFF_PREFIX=<text>`: Signoff prepended to outgoing messages so recipients can tell they were agent-sent, e.g. `π€`. Default: none. CLI equivalent: `--message-signoff-prefix <text>` (see Message Signoff below)
|
|
740
741
|
- `MS365_MCP_MESSAGE_SIGNOFF_SUFFIX=<text>`: Signoff appended to outgoing messages. Default: none. CLI equivalent: `--message-signoff-suffix <text>`. `--no-message-signoff` disables both (see Message Signoff below)
|
|
741
742
|
- `MS365_MCP_RATE_LIMIT_DISABLED=true|1`: Disable per-IP rate limiting in HTTP mode (default: enabled β 30 req/min on `/authorize`, `/token`, `/register`; 120 req/min on `/mcp`)
|
|
@@ -760,6 +761,46 @@ Environment variables:
|
|
|
760
761
|
- `MS365_MCP_EXPECTED_USERNAME`: Require local MSAL auth to use this Microsoft account username (case-insensitive; CLI flag takes precedence)
|
|
761
762
|
- `MS365_MCP_EXPECTED_HOME_ACCOUNT_ID`: Require local MSAL auth to use this exact MSAL homeAccountId (CLI flag takes precedence)
|
|
762
763
|
|
|
764
|
+
## Reading Mail as Clean Text
|
|
765
|
+
|
|
766
|
+
`read-mail-text` returns messages as text with the bulk junk removed. Graph's
|
|
767
|
+
plain-text body is still mostly not the message: links are wrapped in Safe Links
|
|
768
|
+
or tracking redirects several hundred characters long, marketing mail pads its
|
|
769
|
+
preheader with invisible characters, images become `[https://β¦png]` lines, and
|
|
770
|
+
replies carry the whole quoted thread. On a real mailbox, 20 inbox messages came
|
|
771
|
+
to about 280,000 characters through `list-mail-messages` and 70,000 through
|
|
772
|
+
`read-mail-text`.
|
|
773
|
+
|
|
774
|
+
The rule is: remove obvious junk, never cut or rewrite the message; anything
|
|
775
|
+
ambiguous is kept. It removes runs of invisible padding characters, image
|
|
776
|
+
placeholders (a bracketed address of an image file), and quoted reply history
|
|
777
|
+
from its header down (a dated `On β¦ wrote:`, also `>`-quoted, an Outlook
|
|
778
|
+
`From:/Sent:` block, `-----Original Message-----`). Forwards keep the forwarded
|
|
779
|
+
content: a subject starting `FW:` / `Fwd:` or a localised prefix (`VS:`, `WG:`,
|
|
780
|
+
`TR:`, `RV:`, `I:`, `ENC:`, β¦, also after tags such as `[EXTERNAL]`), or history
|
|
781
|
+
directly under a `Forwarded message` / `Begin forwarded message:` marker. Links are kept:
|
|
782
|
+
Safe Links, Proofpoint URL Defense and Google redirects are unwrapped to the real
|
|
783
|
+
address, tracking parameters (`utm_*` and similar) are dropped, and only an
|
|
784
|
+
address still longer than 300 characters (a tracking redirect) is shortened to
|
|
785
|
+
`β¦link: domainβ§`. Line endings become `\n`, and the marker brackets are
|
|
786
|
+
reserved: a body's own `β¦ β§` appear as `[ ]`, so text cannot pose as a marker.
|
|
787
|
+
Everything else β signatures, footers, whitespace, repeated links β is left as
|
|
788
|
+
the message has it. `get-mail-message` returns the raw body.
|
|
789
|
+
|
|
790
|
+
- `messageIds` (up to 20, one `$batch`), or `folderId` with optional `top`
|
|
791
|
+
(max 20), `filter` or `search`.
|
|
792
|
+
- Each message is cut to `maxChars` characters, default 3,000 (marked
|
|
793
|
+
`truncated`): a page of 20 then fits the result limit, and a typical message
|
|
794
|
+
is whole. Ticket systems that repeat their history in every update put the
|
|
795
|
+
newest entry first, so the cut keeps it. Pass a larger `maxChars` (up to
|
|
796
|
+
300,000; a raw body beyond that is cleaned only that far) for the whole of a
|
|
797
|
+
long message; `get-mail-message` has the raw body.
|
|
798
|
+
- Returns `[{ id, from, to, cc, subject, received, hasAttachments, text, chars, truncated }]`;
|
|
799
|
+
an unknown id comes back as `{ id, error }`.
|
|
800
|
+
- A result over `MS365_MCP_READ_MAIL_TEXT_MAX_CHARS` (default 80,000) is refused
|
|
801
|
+
whole with the largest messages listed, so the agent narrows the query instead
|
|
802
|
+
of silently getting part of it.
|
|
803
|
+
|
|
763
804
|
## Server-Minted Attachment URLs
|
|
764
805
|
|
|
765
806
|
`get-download-url` returns Microsoft's own pre-authenticated `@microsoft.graph.downloadUrl`
|
|
@@ -1164,7 +1164,7 @@ describe("graph-tools", () => {
|
|
|
1164
1164
|
expect(request).toContain("$skiptoken=next-token");
|
|
1165
1165
|
});
|
|
1166
1166
|
it.each(["list-calendar-events-delta", "list-calendar-view-delta"])(
|
|
1167
|
-
"
|
|
1167
|
+
"rejects mixed-case top values before Graph dispatch for %s",
|
|
1168
1168
|
async (alias) => {
|
|
1169
1169
|
mockEndpoints.push(makeEndpoint({ alias }));
|
|
1170
1170
|
mockEndpointsJson = [makeConfig({ toolName: alias })];
|
|
@@ -1173,12 +1173,10 @@ describe("graph-tools", () => {
|
|
|
1173
1173
|
const { registerGraphTools } = await loadModule();
|
|
1174
1174
|
registerGraphTools(server, graphClient);
|
|
1175
1175
|
for (const key of ["top", "$top", "TOP", "$TOP", "$Top"]) {
|
|
1176
|
-
await server.tools.get(alias).handler({ [key]: 10 });
|
|
1177
|
-
|
|
1178
|
-
expect(graphClient.graphRequest).toHaveBeenCalledTimes(5);
|
|
1179
|
-
for (const call of graphClient.graphRequest.mock.calls) {
|
|
1180
|
-
expect(JSON.stringify(call)).not.toContain("$top");
|
|
1176
|
+
const result = await server.tools.get(alias).handler({ [key]: 10 });
|
|
1177
|
+
expect(result.isError).toBe(true);
|
|
1181
1178
|
}
|
|
1179
|
+
expect(graphClient.graphRequest).not.toHaveBeenCalled();
|
|
1182
1180
|
}
|
|
1183
1181
|
);
|
|
1184
1182
|
it.each(["list-joined-teams", "list-my-associated-teams"])(
|
|
@@ -1212,7 +1210,7 @@ describe("graph-tools", () => {
|
|
|
1212
1210
|
expect(graphClient.graphRequest).not.toHaveBeenCalled();
|
|
1213
1211
|
});
|
|
1214
1212
|
it("sends a calendar field array as comma-separated text in one Graph request", async () => {
|
|
1215
|
-
const alias = "list-calendar-events
|
|
1213
|
+
const alias = "list-calendar-events";
|
|
1216
1214
|
mockEndpoints.push(makeEndpoint({ alias }));
|
|
1217
1215
|
mockEndpointsJson = [makeConfig({ toolName: alias })];
|
|
1218
1216
|
const graphClient = createMockGraphClient();
|
|
@@ -4190,6 +4188,175 @@ describe("graph-tools", () => {
|
|
|
4190
4188
|
expect(isDestructiveOperation("DELETE", { readOnly: true })).toBe(true);
|
|
4191
4189
|
});
|
|
4192
4190
|
});
|
|
4191
|
+
describe("rewriteMentionShorthand", () => {
|
|
4192
|
+
const U1 = "11111111-2222-3333-4444-555555555555";
|
|
4193
|
+
const U2 = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee";
|
|
4194
|
+
const html = (content, extra = {}) => ({
|
|
4195
|
+
body: { contentType: "html", content },
|
|
4196
|
+
...extra
|
|
4197
|
+
});
|
|
4198
|
+
const mention = (id, userId, name) => ({
|
|
4199
|
+
id,
|
|
4200
|
+
mentionText: name,
|
|
4201
|
+
mentioned: { user: { id: userId, displayName: name, userIdentityType: "aadUser" } }
|
|
4202
|
+
});
|
|
4203
|
+
it("rewrites one tag and builds the mentions array", async () => {
|
|
4204
|
+
const { rewriteMentionShorthand } = await loadModule();
|
|
4205
|
+
expect(rewriteMentionShorthand(html(`Hi <at user-id="${U1}">Jane Doe</at>!`)).body).toEqual(
|
|
4206
|
+
html('Hi <at id="0">Jane Doe</at>!', { mentions: [mention(0, U1, "Jane Doe")] })
|
|
4207
|
+
);
|
|
4208
|
+
});
|
|
4209
|
+
it("numbers several tags in order, including a repeated user", async () => {
|
|
4210
|
+
const { rewriteMentionShorthand } = await loadModule();
|
|
4211
|
+
const out = rewriteMentionShorthand(
|
|
4212
|
+
html(
|
|
4213
|
+
`<at user-id="${U1}">Jane</at> <at user-id="${U2}">Bob</at> <at user-id="${U1}">Jane</at>`
|
|
4214
|
+
)
|
|
4215
|
+
).body;
|
|
4216
|
+
expect(out.body.content).toBe(
|
|
4217
|
+
'<at id="0">Jane</at> <at id="1">Bob</at> <at id="2">Jane</at>'
|
|
4218
|
+
);
|
|
4219
|
+
expect(out.mentions.map((m) => m.id)).toEqual([0, 1, 2]);
|
|
4220
|
+
});
|
|
4221
|
+
it("starts above ids already in the content and mentions", async () => {
|
|
4222
|
+
const { rewriteMentionShorthand } = await loadModule();
|
|
4223
|
+
const input = html(`<at id="0">Old</at> <at user-id="${U1}">Jane</at>`, {
|
|
4224
|
+
mentions: [mention(0, U2, "Old")]
|
|
4225
|
+
});
|
|
4226
|
+
const out = rewriteMentionShorthand(input).body;
|
|
4227
|
+
expect(out.body.content).toBe('<at id="0">Old</at> <at id="1">Jane</at>');
|
|
4228
|
+
expect(out.mentions).toEqual([mention(0, U2, "Old"), mention(1, U1, "Jane")]);
|
|
4229
|
+
});
|
|
4230
|
+
it("passes a correct hand-built body through unchanged", async () => {
|
|
4231
|
+
const { rewriteMentionShorthand } = await loadModule();
|
|
4232
|
+
const input = html('<at id="0">Jane</at>', { mentions: [mention(0, U1, "Jane")] });
|
|
4233
|
+
expect(rewriteMentionShorthand(input).body).toBe(input);
|
|
4234
|
+
});
|
|
4235
|
+
it("returns an error for a user-id that is not a GUID", async () => {
|
|
4236
|
+
const { rewriteMentionShorthand } = await loadModule();
|
|
4237
|
+
const input = html('<at user-id="jane@contoso.com">Jane</at>');
|
|
4238
|
+
const out = rewriteMentionShorthand(input);
|
|
4239
|
+
expect(out.error).toContain("jane@contoso.com");
|
|
4240
|
+
expect(out.body).toBe(input);
|
|
4241
|
+
});
|
|
4242
|
+
it("leaves non-html content without the shorthand alone", async () => {
|
|
4243
|
+
const { rewriteMentionShorthand } = await loadModule();
|
|
4244
|
+
const text = { body: { contentType: "text", content: "plain words" } };
|
|
4245
|
+
const bare = { body: { content: "plain words" } };
|
|
4246
|
+
expect(rewriteMentionShorthand(text).body).toBe(text);
|
|
4247
|
+
expect(rewriteMentionShorthand(bare).body).toBe(bare);
|
|
4248
|
+
});
|
|
4249
|
+
it.each([
|
|
4250
|
+
["text", { contentType: "text" }],
|
|
4251
|
+
["missing", {}]
|
|
4252
|
+
])("returns an error for the shorthand when contentType is %s", async (_label, type) => {
|
|
4253
|
+
const { rewriteMentionShorthand } = await loadModule();
|
|
4254
|
+
const input = { body: { ...type, content: `<at user-id="${U1}">Jane</at>` } };
|
|
4255
|
+
const out = rewriteMentionShorthand(input);
|
|
4256
|
+
expect(out.error).toContain("contentType");
|
|
4257
|
+
expect(out.body).toBe(input);
|
|
4258
|
+
});
|
|
4259
|
+
it.each([
|
|
4260
|
+
["single-quoted user-id", `<at user-id='${U1}'>Jane</at>`],
|
|
4261
|
+
["spaces around =", `<at user-id = "${U1}">Jane</at>`],
|
|
4262
|
+
["an extra attribute", `<at user-id="${U1}" class="x">Jane</at>`],
|
|
4263
|
+
["nested markup in the name", `<at user-id="${U1}"><b>Jane</b></at>`]
|
|
4264
|
+
])("returns an error for an unsupported tag form: %s", async (_label, tag) => {
|
|
4265
|
+
const { rewriteMentionShorthand } = await loadModule();
|
|
4266
|
+
const input = html(`Hi ${tag}`);
|
|
4267
|
+
const out = rewriteMentionShorthand(input);
|
|
4268
|
+
expect(out.error).toContain("Unsupported mention tag form");
|
|
4269
|
+
expect(out.body).toBe(input);
|
|
4270
|
+
});
|
|
4271
|
+
it("leaves a raw string body alone", async () => {
|
|
4272
|
+
const { rewriteMentionShorthand } = await loadModule();
|
|
4273
|
+
expect(rewriteMentionShorthand('{"body":{}}').body).toBe('{"body":{}}');
|
|
4274
|
+
expect(rewriteMentionShorthand(null).body).toBeNull();
|
|
4275
|
+
});
|
|
4276
|
+
it("keeps the display name verbatim", async () => {
|
|
4277
|
+
const { rewriteMentionShorthand } = await loadModule();
|
|
4278
|
+
const out = rewriteMentionShorthand(html(`<at user-id="${U1}">Smith & Co</at>`)).body;
|
|
4279
|
+
expect(out.body.content).toBe('<at id="0">Smith & Co</at>');
|
|
4280
|
+
expect(out.mentions[0].mentionText).toBe("Smith & Co");
|
|
4281
|
+
});
|
|
4282
|
+
});
|
|
4283
|
+
describe("mention shorthand (tool wiring)", () => {
|
|
4284
|
+
const U1 = "11111111-2222-3333-4444-555555555555";
|
|
4285
|
+
const aliases = [
|
|
4286
|
+
["send-channel-message", "post"],
|
|
4287
|
+
["reply-to-channel-message", "post"],
|
|
4288
|
+
["update-channel-message", "patch"],
|
|
4289
|
+
["update-channel-message-reply", "patch"],
|
|
4290
|
+
["send-chat-message", "post"],
|
|
4291
|
+
["reply-to-chat-message", "post"],
|
|
4292
|
+
["update-chat-message", "patch"]
|
|
4293
|
+
];
|
|
4294
|
+
async function call(alias, method, body) {
|
|
4295
|
+
mockEndpoints.push(
|
|
4296
|
+
makeEndpoint({
|
|
4297
|
+
alias,
|
|
4298
|
+
method,
|
|
4299
|
+
path: "/chats/{chat-id}/messages",
|
|
4300
|
+
parameters: [{ name: "body", type: "Body", schema: z.object({}).passthrough() }]
|
|
4301
|
+
})
|
|
4302
|
+
);
|
|
4303
|
+
mockEndpointsJson = [
|
|
4304
|
+
makeConfig({ toolName: alias, method, pathPattern: "/chats/{chat-id}/messages" })
|
|
4305
|
+
];
|
|
4306
|
+
const graphClient = createMockGraphClient([
|
|
4307
|
+
{ content: [{ type: "text", text: JSON.stringify({ id: "m1" }) }] }
|
|
4308
|
+
]);
|
|
4309
|
+
const server = createMockServer();
|
|
4310
|
+
const { registerGraphTools } = await loadModule();
|
|
4311
|
+
registerGraphTools(server, graphClient);
|
|
4312
|
+
const result = await server.tools.get(alias).handler({ "chat-id": "c1", body, confirm: true });
|
|
4313
|
+
return { result, graphClient };
|
|
4314
|
+
}
|
|
4315
|
+
const sent = (graphClient) => JSON.parse(graphClient.graphRequest.mock.calls[0][1].body);
|
|
4316
|
+
it.each(aliases)("rewrites the shorthand on %s", async (alias, method) => {
|
|
4317
|
+
const { graphClient } = await call(alias, method, {
|
|
4318
|
+
body: { contentType: "html", content: `Hi <at user-id="${U1}">Jane</at>` }
|
|
4319
|
+
});
|
|
4320
|
+
const out = sent(graphClient);
|
|
4321
|
+
expect(out.body.content).toBe('Hi <at id="0">Jane</at>');
|
|
4322
|
+
expect(out.mentions).toEqual([
|
|
4323
|
+
{
|
|
4324
|
+
id: 0,
|
|
4325
|
+
mentionText: "Jane",
|
|
4326
|
+
mentioned: { user: { id: U1, displayName: "Jane", userIdentityType: "aadUser" } }
|
|
4327
|
+
}
|
|
4328
|
+
]);
|
|
4329
|
+
});
|
|
4330
|
+
it("returns a tool error and sends nothing for a bad user-id", async () => {
|
|
4331
|
+
const { result, graphClient } = await call("send-chat-message", "post", {
|
|
4332
|
+
body: { contentType: "html", content: '<at user-id="jane">Jane</at>' }
|
|
4333
|
+
});
|
|
4334
|
+
expect(result.isError).toBe(true);
|
|
4335
|
+
expect(result.content[0].text).toContain("not an Entra object id");
|
|
4336
|
+
expect(graphClient.graphRequest).not.toHaveBeenCalled();
|
|
4337
|
+
});
|
|
4338
|
+
it("does not rewrite on a tool outside the seven", async () => {
|
|
4339
|
+
const { graphClient } = await call("create-draft-email", "post", {
|
|
4340
|
+
body: { contentType: "html", content: `<at user-id="${U1}">Jane</at>` }
|
|
4341
|
+
});
|
|
4342
|
+
expect(sent(graphClient).body.content).toBe(`<at user-id="${U1}">Jane</at>`);
|
|
4343
|
+
});
|
|
4344
|
+
it("lists only tools that exist in endpoints.json and documents the shorthand", async () => {
|
|
4345
|
+
const actualFs = await vi.importActual("fs");
|
|
4346
|
+
const endpoints = JSON.parse(
|
|
4347
|
+
actualFs.readFileSync(join(__dirname, "..", "endpoints.json"), "utf8")
|
|
4348
|
+
);
|
|
4349
|
+
for (const [alias] of aliases) {
|
|
4350
|
+
const entry = endpoints.find((e) => e.toolName === alias);
|
|
4351
|
+
expect(entry, alias).toBeDefined();
|
|
4352
|
+
const tip = entry.llmTip;
|
|
4353
|
+
expect(tip, alias).toContain("user-id=");
|
|
4354
|
+
expect(tip, alias).not.toContain("name param");
|
|
4355
|
+
expect(tip, alias).toContain("userId");
|
|
4356
|
+
expect(tip.split("To @mention someone").length - 1, alias).toBe(1);
|
|
4357
|
+
}
|
|
4358
|
+
});
|
|
4359
|
+
});
|
|
4193
4360
|
describe("filterMembersByName", () => {
|
|
4194
4361
|
const body = {
|
|
4195
4362
|
value: [
|
package/dist/endpoints.json
CHANGED
|
@@ -1759,7 +1759,8 @@
|
|
|
1759
1759
|
"method": "patch",
|
|
1760
1760
|
"toolName": "update-chat-message",
|
|
1761
1761
|
"presets": ["teams", "work"],
|
|
1762
|
-
"workScopes": ["Chat.ReadWrite"]
|
|
1762
|
+
"workScopes": ["Chat.ReadWrite"],
|
|
1763
|
+
"llmTip": "To @mention someone, put <at user-id=\"<aad-user-id>\">Display Name</at> in the html content; the server numbers the tag and builds the mentions array. Use the userId field (not id) from list-team-members or list-chat-members. Use contentType html in the body."
|
|
1763
1764
|
},
|
|
1764
1765
|
{
|
|
1765
1766
|
"pathPattern": "/chats/{chat-id}/messages/{chatMessage-id}/hostedContents",
|
|
@@ -1775,7 +1776,7 @@
|
|
|
1775
1776
|
"toolName": "send-chat-message",
|
|
1776
1777
|
"presets": ["teams", "teams-write", "work"],
|
|
1777
1778
|
"workScopes": ["ChatMessage.Send"],
|
|
1778
|
-
"llmTip": "Use contentType 'html' in the body β plain text contentType gets mangled by Graph API.
|
|
1779
|
+
"llmTip": "To @mention someone, put <at user-id=\"<aad-user-id>\">Display Name</at> in the html content; the server numbers the tag and builds the mentions array. Use the userId field (not id) from list-team-members or list-chat-members. Use contentType 'html' in the body β plain text contentType gets mangled by Graph API. Alternatively, hand-build the raw form: put an <at id=\"0\">Name</at> tag in the html content and add a mentions array to the request body, as a sibling of the body property (not a separate top-level tool parameter): { body: { contentType: 'html', content: 'Hi <at id=\"0\">Name</at>' }, mentions: [{ id: 0, mentionText: 'Name', mentioned: { user: { id: '<aad-user-id>', displayName: 'Name', userIdentityType: 'aadUser' } } }] }. Each mention id must match its <at id> value; include userIdentityType: 'aadUser' or Graph returns 400 'value without a type name'."
|
|
1779
1780
|
},
|
|
1780
1781
|
{
|
|
1781
1782
|
"pathPattern": "/me/joinedTeams",
|
|
@@ -1878,7 +1879,8 @@
|
|
|
1878
1879
|
"method": "patch",
|
|
1879
1880
|
"toolName": "update-channel-message",
|
|
1880
1881
|
"presets": ["teams", "work"],
|
|
1881
|
-
"workScopes": ["ChannelMessage.ReadWrite"]
|
|
1882
|
+
"workScopes": ["ChannelMessage.ReadWrite"],
|
|
1883
|
+
"llmTip": "To @mention someone, put <at user-id=\"<aad-user-id>\">Display Name</at> in the html content; the server numbers the tag and builds the mentions array. Use the userId field (not id) from list-team-members or list-chat-members. Use contentType html in the body."
|
|
1882
1884
|
},
|
|
1883
1885
|
{
|
|
1884
1886
|
"pathPattern": "/teams/{team-id}/channels/{channel-id}/messages/{chatMessage-id}/hostedContents",
|
|
@@ -1894,7 +1896,7 @@
|
|
|
1894
1896
|
"toolName": "send-channel-message",
|
|
1895
1897
|
"presets": ["teams", "teams-write", "work"],
|
|
1896
1898
|
"workScopes": ["ChannelMessage.Send"],
|
|
1897
|
-
"llmTip": "Use contentType 'html' in the body β plain text contentType gets mangled by Graph API.
|
|
1899
|
+
"llmTip": "To @mention someone, put <at user-id=\"<aad-user-id>\">Display Name</at> in the html content; the server numbers the tag and builds the mentions array. Use the userId field (not id) from list-team-members or list-chat-members. Use contentType 'html' in the body β plain text contentType gets mangled by Graph API. Alternatively, hand-build the raw form: put an <at id=\"0\">Name</at> tag in the html content and add a mentions array to the request body, as a sibling of the body property (not a separate top-level tool parameter): { body: { contentType: 'html', content: 'Hi <at id=\"0\">Name</at>' }, mentions: [{ id: 0, mentionText: 'Name', mentioned: { user: { id: '<aad-user-id>', displayName: 'Name', userIdentityType: 'aadUser' } } }] }. Each mention id must match its <at id> value; include userIdentityType: 'aadUser' or Graph returns 400 'value without a type name'."
|
|
1898
1900
|
},
|
|
1899
1901
|
{
|
|
1900
1902
|
"pathPattern": "/teams/{team-id}/channels/{channel-id}/messages/{chatMessage-id}/replies",
|
|
@@ -1902,7 +1904,7 @@
|
|
|
1902
1904
|
"toolName": "reply-to-channel-message",
|
|
1903
1905
|
"presets": ["teams", "teams-write", "work"],
|
|
1904
1906
|
"workScopes": ["ChannelMessage.Send"],
|
|
1905
|
-
"llmTip": "Use contentType 'html' in the body β plain text contentType gets mangled by Graph API.
|
|
1907
|
+
"llmTip": "To @mention someone, put <at user-id=\"<aad-user-id>\">Display Name</at> in the html content; the server numbers the tag and builds the mentions array. Use the userId field (not id) from list-team-members or list-chat-members. Use contentType 'html' in the body β plain text contentType gets mangled by Graph API. Alternatively, hand-build the raw form: put an <at id=\"0\">Name</at> tag in the html content and add a mentions array to the request body, as a sibling of the body property (not a separate top-level tool parameter): { body: { contentType: 'html', content: 'Hi <at id=\"0\">Name</at>' }, mentions: [{ id: 0, mentionText: 'Name', mentioned: { user: { id: '<aad-user-id>', displayName: 'Name', userIdentityType: 'aadUser' } } }] }. Each mention id must match its <at id> value; include userIdentityType: 'aadUser' or Graph returns 400 'value without a type name'."
|
|
1906
1908
|
},
|
|
1907
1909
|
{
|
|
1908
1910
|
"pathPattern": "/teams/{team-id}/channels/{channel-id}/messages/{chatMessage-id}/replies",
|
|
@@ -1916,7 +1918,8 @@
|
|
|
1916
1918
|
"method": "patch",
|
|
1917
1919
|
"toolName": "update-channel-message-reply",
|
|
1918
1920
|
"presets": ["teams", "work"],
|
|
1919
|
-
"workScopes": ["ChannelMessage.ReadWrite"]
|
|
1921
|
+
"workScopes": ["ChannelMessage.ReadWrite"],
|
|
1922
|
+
"llmTip": "To @mention someone, put <at user-id=\"<aad-user-id>\">Display Name</at> in the html content; the server numbers the tag and builds the mentions array. Use the userId field (not id) from list-team-members or list-chat-members. Use contentType html in the body."
|
|
1920
1923
|
},
|
|
1921
1924
|
{
|
|
1922
1925
|
"pathPattern": "/teams/{team-id}/members",
|
|
@@ -1982,7 +1985,7 @@
|
|
|
1982
1985
|
"toolName": "reply-to-chat-message",
|
|
1983
1986
|
"presets": ["teams", "work"],
|
|
1984
1987
|
"workScopes": ["ChatMessage.Send"],
|
|
1985
|
-
"llmTip": "Use contentType 'html' in the body β plain text contentType gets mangled by Graph API.
|
|
1988
|
+
"llmTip": "To @mention someone, put <at user-id=\"<aad-user-id>\">Display Name</at> in the html content; the server numbers the tag and builds the mentions array. Use the userId field (not id) from list-team-members or list-chat-members. Use contentType 'html' in the body β plain text contentType gets mangled by Graph API. Alternatively, hand-build the raw form: put an <at id=\"0\">Name</at> tag in the html content and add a mentions array to the request body, as a sibling of the body property (not a separate top-level tool parameter): { body: { contentType: 'html', content: 'Hi <at id=\"0\">Name</at>' }, mentions: [{ id: 0, mentionText: 'Name', mentioned: { user: { id: '<aad-user-id>', displayName: 'Name', userIdentityType: 'aadUser' } } }] }. Each mention id must match its <at id> value; include userIdentityType: 'aadUser' or Graph returns 400 'value without a type name'."
|
|
1986
1989
|
},
|
|
1987
1990
|
{
|
|
1988
1991
|
"pathPattern": "/chats/{chat-id}/messages/{chatMessage-id}/setReaction",
|
|
@@ -2540,7 +2543,7 @@
|
|
|
2540
2543
|
"toolName": "list-calendar-events-delta",
|
|
2541
2544
|
"presets": ["calendar", "outlook", "personal"],
|
|
2542
2545
|
"scopes": ["Calendars.Read"],
|
|
2543
|
-
"llmTip": "Incremental sync of events across the default calendar. First call returns all events plus @odata.deltaLink. Subsequent calls with that link return only additions/updates/removals.
|
|
2546
|
+
"llmTip": "Incremental sync of events across the default calendar. First call returns all events plus @odata.deltaLink. Subsequent calls with that link return only additions/updates/removals. Deltas expire after ~30 days β start over if the server returns 410 Gone. For a time-bounded view with delta semantics, use list-calendar-view-delta instead."
|
|
2544
2547
|
},
|
|
2545
2548
|
{
|
|
2546
2549
|
"pathPattern": "/me/calendarView/delta()",
|
package/dist/graph-tools.js
CHANGED
|
@@ -38,10 +38,8 @@ import {
|
|
|
38
38
|
isSkiptokenApplicable,
|
|
39
39
|
paginationAllowed,
|
|
40
40
|
positiveIntFromEnv,
|
|
41
|
-
shouldOmitTopParam,
|
|
42
41
|
SKIPTOKEN_PARAM_DESCRIPTION,
|
|
43
|
-
TIMEZONE_PARAM_DESCRIPTION
|
|
44
|
-
TOP_UNSUPPORTED_DELTA_TOOLS
|
|
42
|
+
TIMEZONE_PARAM_DESCRIPTION
|
|
45
43
|
} from "./lib/param-descriptions.js";
|
|
46
44
|
import { describePathParam } from "./lib/path-params.js";
|
|
47
45
|
import { queryParameterSchema } from "./lib/query-parameter-schema.js";
|
|
@@ -52,6 +50,18 @@ import {
|
|
|
52
50
|
projectSelectedFields
|
|
53
51
|
} from "./lib/select-projection.js";
|
|
54
52
|
import { parseTeamsUrl } from "./lib/teams-url-parser.js";
|
|
53
|
+
import {
|
|
54
|
+
MAIL_TEXT_DEFAULT_MAX_RESPONSE_CHARS,
|
|
55
|
+
MAIL_TEXT_DEFAULT_MAX_CHARS,
|
|
56
|
+
MAIL_TEXT_MAX_INPUT_CHARS,
|
|
57
|
+
MAIL_TEXT_MAX_IDS,
|
|
58
|
+
MAIL_TEXT_MAX_TOP,
|
|
59
|
+
MAIL_TEXT_SELECT,
|
|
60
|
+
PREFER_TEXT_BODY,
|
|
61
|
+
oversizeError,
|
|
62
|
+
toMailTextEntry,
|
|
63
|
+
validateMailTextArgs
|
|
64
|
+
} from "./lib/mail-text.js";
|
|
55
65
|
import { describeToolSchema, describeUtilityToolSchema } from "./lib/tool-schema.js";
|
|
56
66
|
import {
|
|
57
67
|
restrictUserFieldQuery,
|
|
@@ -397,6 +407,63 @@ function collectRecipients(node, domains, counter, depth = 0) {
|
|
|
397
407
|
collectRecipients(value, domains, counter, depth + 1);
|
|
398
408
|
}
|
|
399
409
|
}
|
|
410
|
+
const MENTION_SHORTHAND_TOOLS = /* @__PURE__ */ new Set([
|
|
411
|
+
"send-channel-message",
|
|
412
|
+
"reply-to-channel-message",
|
|
413
|
+
"update-channel-message",
|
|
414
|
+
"update-channel-message-reply",
|
|
415
|
+
"send-chat-message",
|
|
416
|
+
"reply-to-chat-message",
|
|
417
|
+
"update-chat-message"
|
|
418
|
+
]);
|
|
419
|
+
const AAD_GUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
420
|
+
const SHORTHAND_AT = /<at\s+user-id="([^"]*)">([^<]*)<\/at>/gi;
|
|
421
|
+
const HAS_SHORTHAND = /<at\b[^>]*\buser-id\b/i;
|
|
422
|
+
function rewriteMentionShorthand(body) {
|
|
423
|
+
if (!isPlainObject(body) || !isPlainObject(body.body)) return { body };
|
|
424
|
+
const itemBody = body.body;
|
|
425
|
+
const content = itemBody.content;
|
|
426
|
+
if (typeof content !== "string" || !HAS_SHORTHAND.test(content)) return { body };
|
|
427
|
+
if (String(itemBody.contentType).toLowerCase() !== "html") {
|
|
428
|
+
return {
|
|
429
|
+
body,
|
|
430
|
+
error: 'The <at user-id> mention shorthand needs contentType "html" on the message body. Set body.contentType to "html".'
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
const existing = Array.isArray(body.mentions) ? body.mentions : [];
|
|
434
|
+
let nextId = 0;
|
|
435
|
+
for (const m of existing) {
|
|
436
|
+
if (isPlainObject(m) && typeof m.id === "number") nextId = Math.max(nextId, m.id + 1);
|
|
437
|
+
}
|
|
438
|
+
for (const m of content.matchAll(/<at\s+id="(\d+)"/gi)) {
|
|
439
|
+
nextId = Math.max(nextId, Number(m[1]) + 1);
|
|
440
|
+
}
|
|
441
|
+
const added = [];
|
|
442
|
+
let error;
|
|
443
|
+
const rewritten = content.replace(SHORTHAND_AT, (tag, userId, name) => {
|
|
444
|
+
if (!AAD_GUID.test(userId)) {
|
|
445
|
+
error ?? (error = `Mention user-id "${userId}" is not an Entra object id (GUID). Use the userId from list-team-members or list-chat-members.`);
|
|
446
|
+
return tag;
|
|
447
|
+
}
|
|
448
|
+
const id = nextId++;
|
|
449
|
+
added.push({
|
|
450
|
+
id,
|
|
451
|
+
mentionText: name,
|
|
452
|
+
mentioned: { user: { id: userId, displayName: name, userIdentityType: "aadUser" } }
|
|
453
|
+
});
|
|
454
|
+
return `<at id="${id}">${name}</at>`;
|
|
455
|
+
});
|
|
456
|
+
if (error) return { body, error };
|
|
457
|
+
if (HAS_SHORTHAND.test(rewritten)) {
|
|
458
|
+
return {
|
|
459
|
+
body,
|
|
460
|
+
error: 'Unsupported mention tag form. Write <at user-id="<guid>">Name</at> with double quotes, no other attributes and plain text inside.'
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
return {
|
|
464
|
+
body: { ...body, body: { ...itemBody, content: rewritten }, mentions: [...existing, ...added] }
|
|
465
|
+
};
|
|
466
|
+
}
|
|
400
467
|
function bodyForRecipientWalk(body) {
|
|
401
468
|
if (typeof body !== "string") return body;
|
|
402
469
|
if (!/^\s*[{[]/.test(body)) return void 0;
|
|
@@ -864,6 +931,139 @@ const UTILITY_TOOLS = [
|
|
|
864
931
|
}
|
|
865
932
|
}
|
|
866
933
|
},
|
|
934
|
+
{
|
|
935
|
+
name: "read-mail-text",
|
|
936
|
+
method: "GET",
|
|
937
|
+
path: "tool:read-mail-text",
|
|
938
|
+
searchKeywords: "triage inbox skim many emails at once without quoted history signature footer tracking links",
|
|
939
|
+
description: "Triage view of up to 20 mail messages at once, as text with the bulk junk removed: invisible padding, image placeholders and quoted reply history (forwards keep theirs). Links are kept: Safe Links, Proofpoint and Google redirects are unwrapped to the real address and tracking parameters (utm_* and similar) dropped; only an address still over 300 characters (a tracking redirect) is shortened to \u27E6link: domain\u27E7. Everything else (signatures, footers, whitespace) is left as the message has it; line endings become \\n and \u27E6 \u27E7 in the body itself appear as [ ]. Use get-mail-message for the raw body or the quoted thread. Give messageIds (up to 20), or folderId with optional top and filter or search for the newest messages in a folder. Each message is cut to maxChars characters (default 3,000; marked truncated: true): pass a larger maxChars, up to 300,000, for the whole of a long message; a body over 300,000 characters is cleaned only that far. If the whole result would exceed the server limit (MS365_MCP_READ_MAIL_TEXT_MAX_CHARS, default 80000) the call fails, listing the largest messages, instead of returning part of it. Returns [{ id, from, to, cc, subject, received, hasAttachments, text, chars, truncated }].",
|
|
940
|
+
readOnlyHint: true,
|
|
941
|
+
openWorldHint: true,
|
|
942
|
+
buildSchema: (ctx) => {
|
|
943
|
+
const schema = {
|
|
944
|
+
messageIds: z.array(z.string()).max(MAIL_TEXT_MAX_IDS).optional().describe(
|
|
945
|
+
`Ids of the messages to read (max ${MAIL_TEXT_MAX_IDS}). Give messageIds or folderId, not both.`
|
|
946
|
+
),
|
|
947
|
+
folderId: z.string().optional().describe(
|
|
948
|
+
"Folder to read from: a well-known name (inbox, sentitems, drafts, archive, deleteditems, junkemail) or a folder id."
|
|
949
|
+
),
|
|
950
|
+
top: z.number().int().min(1).max(MAIL_TEXT_MAX_TOP).optional().describe(
|
|
951
|
+
`With folderId: how many messages, newest first (default and max ${MAIL_TEXT_MAX_TOP}).`
|
|
952
|
+
),
|
|
953
|
+
filter: z.string().optional().describe(
|
|
954
|
+
"With folderId: OData $filter, e.g. receivedDateTime ge 2026-10-01T00:00:00Z or from/emailAddress/address eq 'a@example.com'."
|
|
955
|
+
),
|
|
956
|
+
search: z.string().optional().describe(
|
|
957
|
+
'With folderId: KQL search, e.g. from:paul subject:"quarterly report" (quoting is normalised as for list-mail-messages). Graph cannot combine it with filter.'
|
|
958
|
+
),
|
|
959
|
+
maxChars: z.number().int().min(1).max(MAIL_TEXT_MAX_INPUT_CHARS).optional().describe(
|
|
960
|
+
"Cut each message text to this many characters (marked truncated). Default 3,000; pass up to 300,000 for the whole of a long message (a body over 300,000 characters is cleaned only that far, also marked truncated)."
|
|
961
|
+
)
|
|
962
|
+
};
|
|
963
|
+
if (ctx.multiAccount) {
|
|
964
|
+
schema["account"] = z.string().optional().describe(
|
|
965
|
+
"Account to use when multiple Microsoft accounts are configured. Required when multiple accounts exist (see list-accounts)."
|
|
966
|
+
);
|
|
967
|
+
}
|
|
968
|
+
return schema;
|
|
969
|
+
},
|
|
970
|
+
execute: async (params, { graphClient, authManager }) => {
|
|
971
|
+
const fail = (error) => ({
|
|
972
|
+
content: [{ type: "text", text: JSON.stringify({ error }) }],
|
|
973
|
+
isError: true
|
|
974
|
+
});
|
|
975
|
+
const invalid = validateMailTextArgs(params);
|
|
976
|
+
if (invalid) return fail(invalid);
|
|
977
|
+
const ids = params.messageIds;
|
|
978
|
+
const folderId = params.folderId;
|
|
979
|
+
const top = params.top;
|
|
980
|
+
const filter = params.filter;
|
|
981
|
+
const search = params.search;
|
|
982
|
+
const maxChars = params.maxChars ?? MAIL_TEXT_DEFAULT_MAX_CHARS;
|
|
983
|
+
const accountParam = params.account;
|
|
984
|
+
const envLimit = Number(process.env.MS365_MCP_READ_MAIL_TEXT_MAX_CHARS);
|
|
985
|
+
const limit = Number.isInteger(envLimit) && envLimit > 0 ? envLimit : MAIL_TEXT_DEFAULT_MAX_RESPONSE_CHARS;
|
|
986
|
+
try {
|
|
987
|
+
const accountModeError = await checkAccountParamInBearerMode(accountParam, authManager);
|
|
988
|
+
if (accountModeError) return fail(accountModeError);
|
|
989
|
+
let accessToken;
|
|
990
|
+
if (authManager && !authManager.isOAuthModeEnabled() && !getRequestTokens()) {
|
|
991
|
+
accessToken = await authManager.getTokenForAccount(accountParam);
|
|
992
|
+
}
|
|
993
|
+
const requestJson = async (endpoint, options) => {
|
|
994
|
+
const response = await graphClient.graphRequest(endpoint, {
|
|
995
|
+
...options,
|
|
996
|
+
accessToken,
|
|
997
|
+
forceJsonOutput: true
|
|
998
|
+
});
|
|
999
|
+
if (response?.isError) return { error: response };
|
|
1000
|
+
const text2 = response?.content?.[0]?.text;
|
|
1001
|
+
try {
|
|
1002
|
+
return { data: JSON.parse(typeof text2 === "string" ? text2 : "") };
|
|
1003
|
+
} catch {
|
|
1004
|
+
return { error: fail("Unexpected non-JSON response from Microsoft Graph.") };
|
|
1005
|
+
}
|
|
1006
|
+
};
|
|
1007
|
+
let entries;
|
|
1008
|
+
if (ids !== void 0) {
|
|
1009
|
+
const requests = ids.map((id, i) => ({
|
|
1010
|
+
id: String(i),
|
|
1011
|
+
method: "GET",
|
|
1012
|
+
url: `/me/messages/${encodeURIComponent(id)}?$select=${MAIL_TEXT_SELECT}`,
|
|
1013
|
+
headers: { Prefer: PREFER_TEXT_BODY }
|
|
1014
|
+
}));
|
|
1015
|
+
const r = await requestJson("/$batch", {
|
|
1016
|
+
method: "POST",
|
|
1017
|
+
body: JSON.stringify({ requests }),
|
|
1018
|
+
headers: { "Content-Type": "application/json" }
|
|
1019
|
+
});
|
|
1020
|
+
if (r.error) return r.error;
|
|
1021
|
+
const responses = r.data?.responses ?? [];
|
|
1022
|
+
const byId = new Map(responses.map((x) => [x.id, x]));
|
|
1023
|
+
entries = ids.map((id, i) => {
|
|
1024
|
+
const x = byId.get(String(i));
|
|
1025
|
+
if (!x) return { id, error: "no response" };
|
|
1026
|
+
if (x.status !== 200) {
|
|
1027
|
+
const e = x.body?.error;
|
|
1028
|
+
return {
|
|
1029
|
+
id,
|
|
1030
|
+
error: x.status === 404 ? "not found" : `${e?.code ?? x.status}: ${e?.message ?? ""}`.trim()
|
|
1031
|
+
};
|
|
1032
|
+
}
|
|
1033
|
+
return toMailTextEntry(x.body ?? {}, maxChars);
|
|
1034
|
+
});
|
|
1035
|
+
} else {
|
|
1036
|
+
const queryParams = {
|
|
1037
|
+
$select: MAIL_TEXT_SELECT,
|
|
1038
|
+
$top: String(top ?? MAIL_TEXT_MAX_TOP)
|
|
1039
|
+
};
|
|
1040
|
+
if (filter !== void 0) queryParams["$filter"] = filter;
|
|
1041
|
+
if (search !== void 0) {
|
|
1042
|
+
queryParams["$search"] = search;
|
|
1043
|
+
const searchError = normalizeSearchQueryParam(
|
|
1044
|
+
queryParams,
|
|
1045
|
+
"/me/mailFolders/{mailFolder-id}/messages",
|
|
1046
|
+
"read-mail-text"
|
|
1047
|
+
);
|
|
1048
|
+
if (searchError) return searchError;
|
|
1049
|
+
}
|
|
1050
|
+
const r = await requestJson(
|
|
1051
|
+
`/me/mailFolders/${encodeURIComponent(folderId)}/messages?${new URLSearchParams(queryParams)}`,
|
|
1052
|
+
{ headers: { Prefer: PREFER_TEXT_BODY } }
|
|
1053
|
+
);
|
|
1054
|
+
if (r.error) return r.error;
|
|
1055
|
+
entries = (r.data?.value ?? []).map(
|
|
1056
|
+
(m) => toMailTextEntry(m, maxChars)
|
|
1057
|
+
);
|
|
1058
|
+
}
|
|
1059
|
+
const text = JSON.stringify(entries);
|
|
1060
|
+
if (text.length > limit) return fail(oversizeError(entries, text.length, limit));
|
|
1061
|
+
return { content: [{ type: "text", text }] };
|
|
1062
|
+
} catch (error) {
|
|
1063
|
+
return fail(error.message);
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
},
|
|
867
1067
|
{
|
|
868
1068
|
name: "download-bytes",
|
|
869
1069
|
method: "GET",
|
|
@@ -1588,8 +1788,7 @@ async function executeGraphTool(tool, config, graphClient, params, authManager,
|
|
|
1588
1788
|
(p) => p.name === paramName || p.name === camelCaseParamName || isOdataParam && p.name.replace(/^\$/, "").toLowerCase() === normalizedParamName
|
|
1589
1789
|
);
|
|
1590
1790
|
const isQuery = paramDef?.type === "Query" || isOdataParam;
|
|
1591
|
-
|
|
1592
|
-
if (isQuery && !ignoredDeltaTop && paramValue != null && paramValue !== "") {
|
|
1791
|
+
if (isQuery && paramValue != null && paramValue !== "") {
|
|
1593
1792
|
const schema = queryParameterSchema(
|
|
1594
1793
|
tool.alias,
|
|
1595
1794
|
normalizedParamName,
|
|
@@ -1730,8 +1929,15 @@ async function executeGraphTool(tool, config, graphClient, params, authManager,
|
|
|
1730
1929
|
);
|
|
1731
1930
|
}
|
|
1732
1931
|
}
|
|
1733
|
-
if (
|
|
1734
|
-
|
|
1932
|
+
if (MENTION_SHORTHAND_TOOLS.has(tool.alias)) {
|
|
1933
|
+
const rewritten = rewriteMentionShorthand(body);
|
|
1934
|
+
if (rewritten.error) {
|
|
1935
|
+
return {
|
|
1936
|
+
content: [{ type: "text", text: JSON.stringify({ error: rewritten.error }) }],
|
|
1937
|
+
isError: true
|
|
1938
|
+
};
|
|
1939
|
+
}
|
|
1940
|
+
body = rewritten.body;
|
|
1735
1941
|
}
|
|
1736
1942
|
const skiptokenError = normalizeSkiptokenQueryParam(queryParams, tool.alias);
|
|
1737
1943
|
if (skiptokenError) return skiptokenError;
|
|
@@ -2588,5 +2794,6 @@ export {
|
|
|
2588
2794
|
isDestructiveOperation,
|
|
2589
2795
|
registerDiscoveryTools,
|
|
2590
2796
|
registerGraphTools,
|
|
2797
|
+
rewriteMentionShorthand,
|
|
2591
2798
|
scoreDiscoveryQuery
|
|
2592
2799
|
};
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
const MAIL_TEXT_MAX_IDS = 20;
|
|
2
|
+
const MAIL_TEXT_MAX_TOP = 20;
|
|
3
|
+
const MAIL_TEXT_DEFAULT_MAX_RESPONSE_CHARS = 8e4;
|
|
4
|
+
const MAIL_TEXT_DEFAULT_MAX_CHARS = 3e3;
|
|
5
|
+
const MAIL_TEXT_MAX_INPUT_CHARS = 3e5;
|
|
6
|
+
const MAIL_TEXT_SELECT = "id,subject,from,toRecipients,ccRecipients,receivedDateTime,hasAttachments,body";
|
|
7
|
+
const PREFER_TEXT_BODY = 'outlook.body-content-type="text"';
|
|
8
|
+
const QUOTE_START = [
|
|
9
|
+
/^On .{1,300}wrote:\s*$/i,
|
|
10
|
+
// Gmail / Apple Mail
|
|
11
|
+
/^-{2,}\s*Original Message\s*-{2,}/i,
|
|
12
|
+
// Outlook classic
|
|
13
|
+
/^Le .{1,300}a Γ©crit\s*:\s*$/i,
|
|
14
|
+
/^Am .{1,300}schrieb .{0,200}:\s*$/i
|
|
15
|
+
];
|
|
16
|
+
const DATED = [true, false, true, true];
|
|
17
|
+
function isReplyHeader(l) {
|
|
18
|
+
return QUOTE_START.some((r, k) => r.test(l) && (!DATED[k] || /\d/.test(l)));
|
|
19
|
+
}
|
|
20
|
+
const HEADER_FROM = /^\*?From:\*?\s+\S/i;
|
|
21
|
+
const HEADER_NEXT = /^\*?(Sent|Date|To|Subject):\*?\s/i;
|
|
22
|
+
const SEPARATOR = /^_{10,}\s*$/;
|
|
23
|
+
function isHeaderBlock(lines, i) {
|
|
24
|
+
const l = lines[i]?.trim() ?? "";
|
|
25
|
+
if (l.length > 2e3 || !HEADER_FROM.test(l)) return false;
|
|
26
|
+
let headers = 0;
|
|
27
|
+
for (let j = i + 1; j < Math.min(lines.length, i + 6); j++) {
|
|
28
|
+
if (lines[j].length < 2e3 && HEADER_NEXT.test(lines[j].trim())) headers++;
|
|
29
|
+
}
|
|
30
|
+
return headers >= 2;
|
|
31
|
+
}
|
|
32
|
+
const FORWARD_MARKER = /^(?:-{2,}\s*Forwarded message\s*-{2,}|Begin forwarded message:)$/i;
|
|
33
|
+
function forwardMarkerAbove(lines, q) {
|
|
34
|
+
let seen = 0;
|
|
35
|
+
for (let j = q - 1; j >= 0 && seen < 3; j--) {
|
|
36
|
+
const l = lines[j].trim();
|
|
37
|
+
if (!l) continue;
|
|
38
|
+
if (l.length < 100 && FORWARD_MARKER.test(l)) return true;
|
|
39
|
+
seen++;
|
|
40
|
+
}
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
function quoteStart(lines) {
|
|
44
|
+
for (let i = 0; i < lines.length; i++) {
|
|
45
|
+
const l = lines[i].trim();
|
|
46
|
+
if (l.length > 2e3) continue;
|
|
47
|
+
if (isReplyHeader(l)) return i;
|
|
48
|
+
if (l.startsWith(">") && isReplyHeader(l.replace(/^(?:>[ \t]?){1,20}/, "").trim())) return i;
|
|
49
|
+
if (/^On .{1,200}/i.test(l) && /\d/.test(l) && i + 1 < lines.length && l.length + lines[i + 1].length < 320 && /wrote:\s*$/i.test(lines[i + 1].trim())) {
|
|
50
|
+
return i;
|
|
51
|
+
}
|
|
52
|
+
if (isHeaderBlock(lines, i)) return i;
|
|
53
|
+
if (SEPARATOR.test(l)) {
|
|
54
|
+
let j = i + 1;
|
|
55
|
+
while (j < lines.length && j <= i + 2 && !lines[j].trim()) j++;
|
|
56
|
+
if (isHeaderBlock(lines, j)) return i;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return -1;
|
|
60
|
+
}
|
|
61
|
+
const MARK_OPEN = "\u27E6";
|
|
62
|
+
const mark = (host) => `\u27E6link: ${host}\u27E7`;
|
|
63
|
+
const MAX_KEPT_URL = 300;
|
|
64
|
+
const TRACKING_PARAM = /^(utm_[a-z_]+|mc_cid|mc_eid|_hsenc|_hsmi|__hstc|__hssc|__hsfp|mkt_tok|gclid|dclid|fbclid|msclkid|yclid|igshid)$/i;
|
|
65
|
+
const LINKEDIN_TRACKING_PARAM = /^(trackingid|refid|lipi|midtoken|midsig|trk|trkemail)$/i;
|
|
66
|
+
const RUN_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
|
|
67
|
+
function unwrapUrlDefenseV3(u) {
|
|
68
|
+
const m = /^https:\/\/urldefense\.com\/v3\/__(.{1,4096}?)__;([A-Za-z0-9_-]{0,4096})!/.exec(u);
|
|
69
|
+
if (!m) return void 0;
|
|
70
|
+
const replaced = [...Buffer.from(m[2], "base64url").toString("utf8")];
|
|
71
|
+
let next = 0;
|
|
72
|
+
let out = m[1].replace(/\*\*(.)|\*/g, (tok, run) => {
|
|
73
|
+
const n = run === void 0 ? 1 : RUN_CHARS.indexOf(run) + 2;
|
|
74
|
+
if (n < 1) return tok;
|
|
75
|
+
const s = replaced.slice(next, next + n).join("");
|
|
76
|
+
next += n;
|
|
77
|
+
return s;
|
|
78
|
+
});
|
|
79
|
+
out = out.replace(/^(https?:)\/(?!\/)/i, "$1//");
|
|
80
|
+
return new URL(out).href;
|
|
81
|
+
}
|
|
82
|
+
function unwrapOnce(u) {
|
|
83
|
+
const url = new URL(u);
|
|
84
|
+
const host = url.hostname.toLowerCase();
|
|
85
|
+
if (/(^|\.)safelinks\.protection\.outlook\.com$/.test(host)) {
|
|
86
|
+
const w = url.searchParams.get("url");
|
|
87
|
+
return w ? new URL(w).href : void 0;
|
|
88
|
+
}
|
|
89
|
+
if (host === "urldefense.com") return unwrapUrlDefenseV3(u);
|
|
90
|
+
if (host === "urldefense.proofpoint.com") {
|
|
91
|
+
const w = url.searchParams.get("u");
|
|
92
|
+
return w ? new URL(decodeURIComponent(w.replace(/_/g, "/").replace(/-/g, "%"))).href : void 0;
|
|
93
|
+
}
|
|
94
|
+
if (/^(www\.)?google\.[a-z.]{2,6}$/.test(host) && url.pathname === "/url") {
|
|
95
|
+
const w = url.searchParams.get("q") ?? url.searchParams.get("url");
|
|
96
|
+
return w && /^https?:\/\//i.test(w) ? new URL(w).href : void 0;
|
|
97
|
+
}
|
|
98
|
+
return void 0;
|
|
99
|
+
}
|
|
100
|
+
function unwrapLink(u) {
|
|
101
|
+
let real = u;
|
|
102
|
+
for (let i = 0; i < 3; i++) {
|
|
103
|
+
let next;
|
|
104
|
+
try {
|
|
105
|
+
next = unwrapOnce(real);
|
|
106
|
+
} catch {
|
|
107
|
+
next = void 0;
|
|
108
|
+
}
|
|
109
|
+
if (!next || next === real) break;
|
|
110
|
+
real = next;
|
|
111
|
+
}
|
|
112
|
+
try {
|
|
113
|
+
const r = new URL(real);
|
|
114
|
+
const linkedIn = /(^|\.)linkedin\.com$/i.test(r.hostname);
|
|
115
|
+
let dropped = false;
|
|
116
|
+
for (const k of [...r.searchParams.keys()]) {
|
|
117
|
+
if (TRACKING_PARAM.test(k) || linkedIn && LINKEDIN_TRACKING_PARAM.test(k)) {
|
|
118
|
+
r.searchParams.delete(k);
|
|
119
|
+
dropped = true;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return dropped ? r.href : real;
|
|
123
|
+
} catch {
|
|
124
|
+
return real;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
function hostOf(u) {
|
|
128
|
+
try {
|
|
129
|
+
return new URL(u).hostname.replace(/^www\./, "") || "link";
|
|
130
|
+
} catch {
|
|
131
|
+
return "link";
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
function showLink(u) {
|
|
135
|
+
const real = unwrapLink(u);
|
|
136
|
+
return real.length <= MAX_KEPT_URL ? real : mark(hostOf(real));
|
|
137
|
+
}
|
|
138
|
+
function splitUrlTail(u) {
|
|
139
|
+
let opens = 0;
|
|
140
|
+
let closes = 0;
|
|
141
|
+
for (const c of u) {
|
|
142
|
+
if (c === "(") opens++;
|
|
143
|
+
else if (c === ")") closes++;
|
|
144
|
+
}
|
|
145
|
+
let end = u.length;
|
|
146
|
+
while (end > 0) {
|
|
147
|
+
const c = u[end - 1];
|
|
148
|
+
if (".,;:!?".includes(c)) {
|
|
149
|
+
end--;
|
|
150
|
+
} else if (c === ")" && closes > opens) {
|
|
151
|
+
end--;
|
|
152
|
+
closes--;
|
|
153
|
+
} else {
|
|
154
|
+
break;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return [u.slice(0, end), u.slice(end)];
|
|
158
|
+
}
|
|
159
|
+
function isImageUrl(u) {
|
|
160
|
+
try {
|
|
161
|
+
return /\.(png|jpe?g|gif|webp|svg|bmp|ico)$/i.test(new URL(u).pathname);
|
|
162
|
+
} catch {
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
function cleanLinks(t) {
|
|
167
|
+
t = t.replace(/<((?:https?|mailto|tel):[^<>\s]{1,4096})>/gi, (m, u) => {
|
|
168
|
+
if (/^(mailto|tel):/i.test(u)) return m;
|
|
169
|
+
const shown = showLink(u);
|
|
170
|
+
return shown.startsWith(MARK_OPEN) ? shown : `<${shown}>`;
|
|
171
|
+
});
|
|
172
|
+
return t.replace(
|
|
173
|
+
/(^|[^<])\b(https?:\/\/[^\s<>"'[\]\u27e6\u27e7]{1,4096})/gi,
|
|
174
|
+
(_m, pre, raw) => {
|
|
175
|
+
const [u, tail] = splitUrlTail(raw);
|
|
176
|
+
return pre + showLink(u) + tail;
|
|
177
|
+
}
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
const IMAGE_PLACEHOLDER = /\[[ \t]{0,20}(https?:\/\/[^\s<>"'[\]\u27e6\u27e7]{1,4096})[ \t]{0,20}\]/gi;
|
|
181
|
+
function cleanMailText(raw, opts = {}) {
|
|
182
|
+
const t = String(raw ?? "").replace(/\r\n?/g, "\n").replace(/(?:(?:[\u200b-\u200d\u2060\ufeff\u00ad]|\u034f)[ \t\u00a0\u2007\u202f]*){3,}/g, " ").replace(/\u27e6/g, "[").replace(/\u27e7/g, "]");
|
|
183
|
+
let lines = t.split("\n");
|
|
184
|
+
if (!opts.keepQuoted) {
|
|
185
|
+
const q = quoteStart(lines);
|
|
186
|
+
if (q >= 0 && lines.slice(0, q).join("").trim() && !forwardMarkerAbove(lines, q)) {
|
|
187
|
+
lines = lines.slice(0, q).concat("");
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
const body = lines.join("\n").replace(IMAGE_PLACEHOLDER, (m, u) => isImageUrl(u) ? "" : m);
|
|
191
|
+
return cleanLinks(body);
|
|
192
|
+
}
|
|
193
|
+
function validateMailTextArgs(p) {
|
|
194
|
+
const isInt = (v, min, max = Number.MAX_SAFE_INTEGER) => typeof v === "number" && Number.isInteger(v) && v >= min && v <= max;
|
|
195
|
+
const hasIds = p.messageIds !== void 0;
|
|
196
|
+
const hasFolder = p.folderId !== void 0;
|
|
197
|
+
if (hasIds === hasFolder) return "Give messageIds or folderId (exactly one).";
|
|
198
|
+
if (p.maxChars !== void 0 && !isInt(p.maxChars, 1, MAIL_TEXT_MAX_INPUT_CHARS)) {
|
|
199
|
+
return `maxChars must be an integer from 1 to ${MAIL_TEXT_MAX_INPUT_CHARS}.`;
|
|
200
|
+
}
|
|
201
|
+
if (hasIds) {
|
|
202
|
+
const ids = p.messageIds;
|
|
203
|
+
if (!Array.isArray(ids) || ids.length === 0 || ids.some((x) => typeof x !== "string" || !x.trim())) {
|
|
204
|
+
return "messageIds must be a non-empty list of message ids.";
|
|
205
|
+
}
|
|
206
|
+
if (ids.length > MAIL_TEXT_MAX_IDS) return `At most ${MAIL_TEXT_MAX_IDS} messageIds per call.`;
|
|
207
|
+
if (p.top !== void 0 || p.filter !== void 0 || p.search !== void 0) {
|
|
208
|
+
return "top, filter and search apply only with folderId.";
|
|
209
|
+
}
|
|
210
|
+
return void 0;
|
|
211
|
+
}
|
|
212
|
+
if (typeof p.folderId !== "string" || !p.folderId.trim()) {
|
|
213
|
+
return "folderId must be a non-empty string.";
|
|
214
|
+
}
|
|
215
|
+
if (p.top !== void 0 && !isInt(p.top, 1, MAIL_TEXT_MAX_TOP)) {
|
|
216
|
+
return `top must be an integer from 1 to ${MAIL_TEXT_MAX_TOP}.`;
|
|
217
|
+
}
|
|
218
|
+
for (const k of ["filter", "search"]) {
|
|
219
|
+
const v = p[k];
|
|
220
|
+
if (v !== void 0 && (typeof v !== "string" || !v.trim())) {
|
|
221
|
+
return `${k} must be a non-empty string (omit it to read the newest messages).`;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
if (p.filter !== void 0 && p.search !== void 0) {
|
|
225
|
+
return "Microsoft Graph cannot combine filter and search on messages; use one.";
|
|
226
|
+
}
|
|
227
|
+
return void 0;
|
|
228
|
+
}
|
|
229
|
+
function formatAddress(r) {
|
|
230
|
+
const e = r?.emailAddress;
|
|
231
|
+
if (!e) return "";
|
|
232
|
+
return e.name && e.address && e.name !== e.address ? `${e.name} <${e.address}>` : e.address || e.name || "";
|
|
233
|
+
}
|
|
234
|
+
const FORWARD_SUBJECT = new RegExp(
|
|
235
|
+
"^\\s*(?:\\[[^\\]\\n]{0,60}\\]\\s*){0,5}(?:fwd?|vs|vb|wg|tr|trs|rv|i|enc|doorst|vl|ed|fs|pd|yml|ilt|\u0130lt|tov\xE1bb\xEDt\xE1s|p\u0101rs\\.|\u03C0\u03C1\u03B8|\u8F6C\u53D1|\u8F49\u5BC4|\u05D4\u05D5\u05E2\u05D1\u05E8|\u05D4\u05E2\u05D1\u05E8\u05D4|\u0625\u0639\u0627\u062F\u0629 \u062A\u0648\u062C\u064A\u0647|\u0BAE\u0BC1\u0BA9\u0BCD\u0BA9\u0BA9\u0BC1\u0BAA\u0BCD\u0BAA\u0BC1)\\s*[:\uFF1A]",
|
|
236
|
+
"iu"
|
|
237
|
+
);
|
|
238
|
+
function isForwardSubject(subject) {
|
|
239
|
+
return FORWARD_SUBJECT.test(subject.slice(0, 500).normalize("NFC"));
|
|
240
|
+
}
|
|
241
|
+
function cutText(s, n) {
|
|
242
|
+
if (s.length <= n) return s;
|
|
243
|
+
const c = s.charCodeAt(n - 1);
|
|
244
|
+
return s.slice(0, c >= 55296 && c <= 56319 ? n - 1 : n);
|
|
245
|
+
}
|
|
246
|
+
function toMailTextEntry(m, maxChars) {
|
|
247
|
+
const subject = m.subject ?? "";
|
|
248
|
+
const keepQuoted = isForwardSubject(subject);
|
|
249
|
+
let raw = m.body?.content ?? "";
|
|
250
|
+
let truncated = false;
|
|
251
|
+
if (raw.length > MAIL_TEXT_MAX_INPUT_CHARS) {
|
|
252
|
+
raw = cutText(raw, MAIL_TEXT_MAX_INPUT_CHARS);
|
|
253
|
+
truncated = true;
|
|
254
|
+
}
|
|
255
|
+
let text = cleanMailText(raw, { keepQuoted });
|
|
256
|
+
const chars = text.length;
|
|
257
|
+
if (maxChars && chars > maxChars) {
|
|
258
|
+
text = cutText(text, maxChars);
|
|
259
|
+
truncated = true;
|
|
260
|
+
}
|
|
261
|
+
return {
|
|
262
|
+
id: m.id ?? "",
|
|
263
|
+
from: formatAddress(m.from),
|
|
264
|
+
to: (m.toRecipients ?? []).map(formatAddress),
|
|
265
|
+
cc: (m.ccRecipients ?? []).map(formatAddress),
|
|
266
|
+
subject,
|
|
267
|
+
received: m.receivedDateTime,
|
|
268
|
+
hasAttachments: !!m.hasAttachments,
|
|
269
|
+
text,
|
|
270
|
+
chars,
|
|
271
|
+
truncated
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
function oversizeError(entries, size, limit) {
|
|
275
|
+
const fmt = (n) => n.toLocaleString("en-US");
|
|
276
|
+
const largest = entries.filter((e) => "text" in e).sort((a, b) => b.text.length - a.text.length).slice(0, 5).map((e) => `"${(e.subject || "(no subject)").slice(0, 60)}" ${fmt(e.text.length)}`).join(", ");
|
|
277
|
+
return `Response would be ${fmt(size)} characters (limit ${fmt(limit)}). Largest: ${largest}. Narrow the query (fewer messages via top, a filter such as a date range or sender, specific messageIds) or set a smaller maxChars.`;
|
|
278
|
+
}
|
|
279
|
+
export {
|
|
280
|
+
MAIL_TEXT_DEFAULT_MAX_CHARS,
|
|
281
|
+
MAIL_TEXT_DEFAULT_MAX_RESPONSE_CHARS,
|
|
282
|
+
MAIL_TEXT_MAX_IDS,
|
|
283
|
+
MAIL_TEXT_MAX_INPUT_CHARS,
|
|
284
|
+
MAIL_TEXT_MAX_TOP,
|
|
285
|
+
MAIL_TEXT_SELECT,
|
|
286
|
+
PREFER_TEXT_BODY,
|
|
287
|
+
cleanMailText,
|
|
288
|
+
cutText,
|
|
289
|
+
isForwardSubject,
|
|
290
|
+
oversizeError,
|
|
291
|
+
toMailTextEntry,
|
|
292
|
+
validateMailTextArgs
|
|
293
|
+
};
|
|
@@ -1,10 +1,23 @@
|
|
|
1
1
|
import logger from "../logger.js";
|
|
2
|
-
const
|
|
3
|
-
|
|
4
|
-
"list-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
2
|
+
const SUPPORTED_QUERY_PARAMS = {
|
|
3
|
+
// https://learn.microsoft.com/en-us/graph/api/user-list-joinedteams
|
|
4
|
+
"list-joined-teams": [],
|
|
5
|
+
// https://learn.microsoft.com/en-us/graph/api/associatedteaminfo-list
|
|
6
|
+
"list-my-associated-teams": [],
|
|
7
|
+
// Documents only $top/$filter; $select is needed to opt in to image bytes.
|
|
8
|
+
// https://learn.microsoft.com/graph/api/teamworkmessaging-list-customemojis?view=graph-rest-beta
|
|
9
|
+
"list-custom-emojis": ["top", "filter", "select", "skiptoken"],
|
|
10
|
+
// https://learn.microsoft.com/en-us/graph/api/channel-list-messages
|
|
11
|
+
"list-channel-messages": ["top", "expand", "skiptoken"],
|
|
12
|
+
// https://learn.microsoft.com/en-us/graph/api/chatmessage-list-replies
|
|
13
|
+
"list-channel-message-replies": ["top", "skiptoken"],
|
|
14
|
+
// Delta rejects $select/$expand/$filter/$orderby/$search and documents no $top/$skip/$count;
|
|
15
|
+
// events delta takes no endDateTime. https://learn.microsoft.com/graph/api/event-delta?view=graph-rest-beta
|
|
16
|
+
"list-calendar-events-delta": ["startdatetime", "skiptoken"],
|
|
17
|
+
"list-calendar-view-delta": ["startdatetime", "enddatetime", "skiptoken"]
|
|
18
|
+
};
|
|
19
|
+
function isQueryParamSupported(toolAlias, bareName) {
|
|
20
|
+
return SUPPORTED_QUERY_PARAMS[toolAlias]?.includes(bareName) ?? true;
|
|
8
21
|
}
|
|
9
22
|
function paginationAllowed() {
|
|
10
23
|
const raw = process.env.MS365_MCP_ALLOW_PAGINATION;
|
|
@@ -39,6 +52,8 @@ const SKIPTOKEN_PARAM_DESCRIPTION = "Next page: the @odata.nextLink from the pre
|
|
|
39
52
|
const COLLECTION_QUERY_PARAMS = /* @__PURE__ */ new Set(["top", "filter", "orderby", "count"]);
|
|
40
53
|
function isSkiptokenApplicable(tool, paramNames) {
|
|
41
54
|
if (tool.method.toUpperCase() !== "GET") return false;
|
|
55
|
+
const supported = tool.alias ? SUPPORTED_QUERY_PARAMS[tool.alias] : void 0;
|
|
56
|
+
if (supported) return supported.includes("skiptoken");
|
|
42
57
|
for (const name of paramNames) {
|
|
43
58
|
if (COLLECTION_QUERY_PARAMS.has(name.replace(/^\$/, "").toLowerCase())) return true;
|
|
44
59
|
}
|
|
@@ -95,15 +110,14 @@ export {
|
|
|
95
110
|
SKIP_PARAM_DESCRIPTION,
|
|
96
111
|
TIMEZONE_PARAM_DESCRIPTION,
|
|
97
112
|
TOP_PARAM_DESCRIPTION,
|
|
98
|
-
TOP_UNSUPPORTED_DELTA_TOOLS,
|
|
99
113
|
getAcceptParamDescription,
|
|
100
114
|
getAccountParamDescription,
|
|
101
115
|
getFetchAllPagesParamDescription,
|
|
102
116
|
getMaxPages,
|
|
103
117
|
getODataParamDescription,
|
|
104
118
|
isFetchAllPagesApplicable,
|
|
119
|
+
isQueryParamSupported,
|
|
105
120
|
isSkiptokenApplicable,
|
|
106
121
|
paginationAllowed,
|
|
107
|
-
positiveIntFromEnv
|
|
108
|
-
shouldOmitTopParam
|
|
122
|
+
positiveIntFromEnv
|
|
109
123
|
};
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { getODataParamDescription,
|
|
3
|
-
const TEAM_LIST_TOOLS = /* @__PURE__ */ new Set(["list-joined-teams", "list-my-associated-teams"]);
|
|
2
|
+
import { getODataParamDescription, isQueryParamSupported } from "./param-descriptions.js";
|
|
4
3
|
const CUSTOM_EMOJI_QUERY_DESCRIPTIONS = {
|
|
5
4
|
top: "Number of custom emojis to return in one page. Base64 image content is opt-in with select; use a small page size when selecting contentBytes.",
|
|
6
5
|
filter: "OData filter expression for custom emojis, forwarded to Microsoft Graph. Filter support is determined by the beta API.",
|
|
@@ -8,11 +7,7 @@ const CUSTOM_EMOJI_QUERY_DESCRIPTIONS = {
|
|
|
8
7
|
};
|
|
9
8
|
function queryParameterSchema(toolName, name, providerSchema) {
|
|
10
9
|
const bareName = name.replace(/^\$/, "").toLowerCase();
|
|
11
|
-
if (
|
|
12
|
-
if (toolName === "list-custom-emojis" && !["top", "filter", "select", "skiptoken"].includes(bareName)) {
|
|
13
|
-
return void 0;
|
|
14
|
-
}
|
|
15
|
-
if (bareName === "top" && shouldOmitTopParam(toolName)) return void 0;
|
|
10
|
+
if (!isQueryParamSupported(toolName, bareName)) return void 0;
|
|
16
11
|
const source = providerSchema instanceof z.ZodOptional ? providerSchema.unwrap() : providerSchema;
|
|
17
12
|
let schema = source;
|
|
18
13
|
switch (bareName) {
|
package/dist/lib/tool-schema.js
CHANGED
package/dist/tool-categories.js
CHANGED
|
@@ -64,7 +64,8 @@ const SCOPED_UTILITY_TOOLS = {
|
|
|
64
64
|
"get-download-url": ["files", "onedrive", "personal", "work", "search"],
|
|
65
65
|
"get-upload-url": ["mail", "calendar", "outlook", "personal", "work"],
|
|
66
66
|
"upload-attachment-from-file": ["mail", "calendar", "outlook", "personal", "work"],
|
|
67
|
-
"parse-teams-url": ["teams", "teams-write", "work"]
|
|
67
|
+
"parse-teams-url": ["teams", "teams-write", "work"],
|
|
68
|
+
"read-mail-text": ["mail", "outlook", "personal", "work"]
|
|
68
69
|
};
|
|
69
70
|
for (const [tool, presets] of Object.entries(SCOPED_UTILITY_TOOLS)) {
|
|
70
71
|
for (const preset of presets) {
|
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.169.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
|
@@ -1759,7 +1759,8 @@
|
|
|
1759
1759
|
"method": "patch",
|
|
1760
1760
|
"toolName": "update-chat-message",
|
|
1761
1761
|
"presets": ["teams", "work"],
|
|
1762
|
-
"workScopes": ["Chat.ReadWrite"]
|
|
1762
|
+
"workScopes": ["Chat.ReadWrite"],
|
|
1763
|
+
"llmTip": "To @mention someone, put <at user-id=\"<aad-user-id>\">Display Name</at> in the html content; the server numbers the tag and builds the mentions array. Use the userId field (not id) from list-team-members or list-chat-members. Use contentType html in the body."
|
|
1763
1764
|
},
|
|
1764
1765
|
{
|
|
1765
1766
|
"pathPattern": "/chats/{chat-id}/messages/{chatMessage-id}/hostedContents",
|
|
@@ -1775,7 +1776,7 @@
|
|
|
1775
1776
|
"toolName": "send-chat-message",
|
|
1776
1777
|
"presets": ["teams", "teams-write", "work"],
|
|
1777
1778
|
"workScopes": ["ChatMessage.Send"],
|
|
1778
|
-
"llmTip": "Use contentType 'html' in the body β plain text contentType gets mangled by Graph API.
|
|
1779
|
+
"llmTip": "To @mention someone, put <at user-id=\"<aad-user-id>\">Display Name</at> in the html content; the server numbers the tag and builds the mentions array. Use the userId field (not id) from list-team-members or list-chat-members. Use contentType 'html' in the body β plain text contentType gets mangled by Graph API. Alternatively, hand-build the raw form: put an <at id=\"0\">Name</at> tag in the html content and add a mentions array to the request body, as a sibling of the body property (not a separate top-level tool parameter): { body: { contentType: 'html', content: 'Hi <at id=\"0\">Name</at>' }, mentions: [{ id: 0, mentionText: 'Name', mentioned: { user: { id: '<aad-user-id>', displayName: 'Name', userIdentityType: 'aadUser' } } }] }. Each mention id must match its <at id> value; include userIdentityType: 'aadUser' or Graph returns 400 'value without a type name'."
|
|
1779
1780
|
},
|
|
1780
1781
|
{
|
|
1781
1782
|
"pathPattern": "/me/joinedTeams",
|
|
@@ -1878,7 +1879,8 @@
|
|
|
1878
1879
|
"method": "patch",
|
|
1879
1880
|
"toolName": "update-channel-message",
|
|
1880
1881
|
"presets": ["teams", "work"],
|
|
1881
|
-
"workScopes": ["ChannelMessage.ReadWrite"]
|
|
1882
|
+
"workScopes": ["ChannelMessage.ReadWrite"],
|
|
1883
|
+
"llmTip": "To @mention someone, put <at user-id=\"<aad-user-id>\">Display Name</at> in the html content; the server numbers the tag and builds the mentions array. Use the userId field (not id) from list-team-members or list-chat-members. Use contentType html in the body."
|
|
1882
1884
|
},
|
|
1883
1885
|
{
|
|
1884
1886
|
"pathPattern": "/teams/{team-id}/channels/{channel-id}/messages/{chatMessage-id}/hostedContents",
|
|
@@ -1894,7 +1896,7 @@
|
|
|
1894
1896
|
"toolName": "send-channel-message",
|
|
1895
1897
|
"presets": ["teams", "teams-write", "work"],
|
|
1896
1898
|
"workScopes": ["ChannelMessage.Send"],
|
|
1897
|
-
"llmTip": "Use contentType 'html' in the body β plain text contentType gets mangled by Graph API.
|
|
1899
|
+
"llmTip": "To @mention someone, put <at user-id=\"<aad-user-id>\">Display Name</at> in the html content; the server numbers the tag and builds the mentions array. Use the userId field (not id) from list-team-members or list-chat-members. Use contentType 'html' in the body β plain text contentType gets mangled by Graph API. Alternatively, hand-build the raw form: put an <at id=\"0\">Name</at> tag in the html content and add a mentions array to the request body, as a sibling of the body property (not a separate top-level tool parameter): { body: { contentType: 'html', content: 'Hi <at id=\"0\">Name</at>' }, mentions: [{ id: 0, mentionText: 'Name', mentioned: { user: { id: '<aad-user-id>', displayName: 'Name', userIdentityType: 'aadUser' } } }] }. Each mention id must match its <at id> value; include userIdentityType: 'aadUser' or Graph returns 400 'value without a type name'."
|
|
1898
1900
|
},
|
|
1899
1901
|
{
|
|
1900
1902
|
"pathPattern": "/teams/{team-id}/channels/{channel-id}/messages/{chatMessage-id}/replies",
|
|
@@ -1902,7 +1904,7 @@
|
|
|
1902
1904
|
"toolName": "reply-to-channel-message",
|
|
1903
1905
|
"presets": ["teams", "teams-write", "work"],
|
|
1904
1906
|
"workScopes": ["ChannelMessage.Send"],
|
|
1905
|
-
"llmTip": "Use contentType 'html' in the body β plain text contentType gets mangled by Graph API.
|
|
1907
|
+
"llmTip": "To @mention someone, put <at user-id=\"<aad-user-id>\">Display Name</at> in the html content; the server numbers the tag and builds the mentions array. Use the userId field (not id) from list-team-members or list-chat-members. Use contentType 'html' in the body β plain text contentType gets mangled by Graph API. Alternatively, hand-build the raw form: put an <at id=\"0\">Name</at> tag in the html content and add a mentions array to the request body, as a sibling of the body property (not a separate top-level tool parameter): { body: { contentType: 'html', content: 'Hi <at id=\"0\">Name</at>' }, mentions: [{ id: 0, mentionText: 'Name', mentioned: { user: { id: '<aad-user-id>', displayName: 'Name', userIdentityType: 'aadUser' } } }] }. Each mention id must match its <at id> value; include userIdentityType: 'aadUser' or Graph returns 400 'value without a type name'."
|
|
1906
1908
|
},
|
|
1907
1909
|
{
|
|
1908
1910
|
"pathPattern": "/teams/{team-id}/channels/{channel-id}/messages/{chatMessage-id}/replies",
|
|
@@ -1916,7 +1918,8 @@
|
|
|
1916
1918
|
"method": "patch",
|
|
1917
1919
|
"toolName": "update-channel-message-reply",
|
|
1918
1920
|
"presets": ["teams", "work"],
|
|
1919
|
-
"workScopes": ["ChannelMessage.ReadWrite"]
|
|
1921
|
+
"workScopes": ["ChannelMessage.ReadWrite"],
|
|
1922
|
+
"llmTip": "To @mention someone, put <at user-id=\"<aad-user-id>\">Display Name</at> in the html content; the server numbers the tag and builds the mentions array. Use the userId field (not id) from list-team-members or list-chat-members. Use contentType html in the body."
|
|
1920
1923
|
},
|
|
1921
1924
|
{
|
|
1922
1925
|
"pathPattern": "/teams/{team-id}/members",
|
|
@@ -1982,7 +1985,7 @@
|
|
|
1982
1985
|
"toolName": "reply-to-chat-message",
|
|
1983
1986
|
"presets": ["teams", "work"],
|
|
1984
1987
|
"workScopes": ["ChatMessage.Send"],
|
|
1985
|
-
"llmTip": "Use contentType 'html' in the body β plain text contentType gets mangled by Graph API.
|
|
1988
|
+
"llmTip": "To @mention someone, put <at user-id=\"<aad-user-id>\">Display Name</at> in the html content; the server numbers the tag and builds the mentions array. Use the userId field (not id) from list-team-members or list-chat-members. Use contentType 'html' in the body β plain text contentType gets mangled by Graph API. Alternatively, hand-build the raw form: put an <at id=\"0\">Name</at> tag in the html content and add a mentions array to the request body, as a sibling of the body property (not a separate top-level tool parameter): { body: { contentType: 'html', content: 'Hi <at id=\"0\">Name</at>' }, mentions: [{ id: 0, mentionText: 'Name', mentioned: { user: { id: '<aad-user-id>', displayName: 'Name', userIdentityType: 'aadUser' } } }] }. Each mention id must match its <at id> value; include userIdentityType: 'aadUser' or Graph returns 400 'value without a type name'."
|
|
1986
1989
|
},
|
|
1987
1990
|
{
|
|
1988
1991
|
"pathPattern": "/chats/{chat-id}/messages/{chatMessage-id}/setReaction",
|
|
@@ -2540,7 +2543,7 @@
|
|
|
2540
2543
|
"toolName": "list-calendar-events-delta",
|
|
2541
2544
|
"presets": ["calendar", "outlook", "personal"],
|
|
2542
2545
|
"scopes": ["Calendars.Read"],
|
|
2543
|
-
"llmTip": "Incremental sync of events across the default calendar. First call returns all events plus @odata.deltaLink. Subsequent calls with that link return only additions/updates/removals.
|
|
2546
|
+
"llmTip": "Incremental sync of events across the default calendar. First call returns all events plus @odata.deltaLink. Subsequent calls with that link return only additions/updates/removals. Deltas expire after ~30 days β start over if the server returns 410 Gone. For a time-bounded view with delta semantics, use list-calendar-view-delta instead."
|
|
2544
2547
|
},
|
|
2545
2548
|
{
|
|
2546
2549
|
"pathPattern": "/me/calendarView/delta()",
|