@softeria/ms-365-mcp-server 0.96.0 → 0.98.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/modules/generate-mcp-tools.mjs +7 -0
- package/dist/__tests__/graph-tools.test.js +284 -0
- package/dist/endpoints.json +6 -51
- package/dist/generated/client.js +5 -75
- package/dist/graph-client.js +1 -0
- package/dist/graph-tools.js +237 -75
- package/dist/lib/tool-schema.js +24 -1
- package/dist/mcp-instructions.js +2 -1
- package/dist/server.js +3 -1
- package/dist/tool-categories.js +4 -4
- package/package.json +1 -1
- package/src/endpoints.json +6 -51
- package/src/generated/endpoint-types.ts +1 -1
|
@@ -51,6 +51,13 @@ export function generateMcpTools(openApiSpec, outputDir) {
|
|
|
51
51
|
// Replace with: path: `/...range(param=':value')...`,
|
|
52
52
|
clientCode = clientCode.replace(/(path:\s*)'(\/[^']*\([^)]*=':[\w]+'\)[^']*)'/g, '$1`$2`');
|
|
53
53
|
|
|
54
|
+
// openapi-zod-client emits z.instanceof(File) for `format: binary` bodies; MCP
|
|
55
|
+
// transports JSON so no caller produces File. Body marshaller decodes the string.
|
|
56
|
+
clientCode = clientCode.replace(
|
|
57
|
+
/z\.instanceof\(File\)/g,
|
|
58
|
+
"z.string().describe('Base64-encoded file content. The server decodes it and PUTs the raw bytes to Microsoft Graph.')"
|
|
59
|
+
);
|
|
60
|
+
|
|
54
61
|
fs.writeFileSync(clientFilePath, clientCode);
|
|
55
62
|
|
|
56
63
|
return true;
|
|
@@ -441,4 +441,288 @@ describe("graph-tools", () => {
|
|
|
441
441
|
expect(prefer === void 0 || !prefer.includes("outlook.body-content-type")).toBe(true);
|
|
442
442
|
});
|
|
443
443
|
});
|
|
444
|
+
describe("binary upload bodies", () => {
|
|
445
|
+
it("decodes base64 body to bytes and sets octet-stream Content-Type", async () => {
|
|
446
|
+
const endpoint = makeEndpoint({
|
|
447
|
+
alias: "upload-file-content",
|
|
448
|
+
method: "put",
|
|
449
|
+
path: "/drives/:driveId/items/:driveItemId/content",
|
|
450
|
+
requestFormat: "binary",
|
|
451
|
+
parameters: [
|
|
452
|
+
{ name: "driveId", type: "Path", schema: z.string() },
|
|
453
|
+
{ name: "driveItemId", type: "Path", schema: z.string() },
|
|
454
|
+
{
|
|
455
|
+
name: "body",
|
|
456
|
+
type: "Body",
|
|
457
|
+
schema: z.string().describe("Base64-encoded file content")
|
|
458
|
+
}
|
|
459
|
+
]
|
|
460
|
+
});
|
|
461
|
+
const config = makeConfig({
|
|
462
|
+
toolName: "upload-file-content",
|
|
463
|
+
method: "put",
|
|
464
|
+
pathPattern: "/drives/{drive-id}/items/{driveItem-id}/content",
|
|
465
|
+
scopes: ["Files.ReadWrite"]
|
|
466
|
+
});
|
|
467
|
+
mockEndpoints.push(endpoint);
|
|
468
|
+
mockEndpointsJson = [config];
|
|
469
|
+
const graphClient = createMockGraphClient([{ content: [{ type: "text", text: "{}" }] }]);
|
|
470
|
+
const server = createMockServer();
|
|
471
|
+
const { registerGraphTools } = await loadModule();
|
|
472
|
+
registerGraphTools(server, graphClient);
|
|
473
|
+
const original = "Hello, world!";
|
|
474
|
+
const base64 = Buffer.from(original, "utf-8").toString("base64");
|
|
475
|
+
await server.tools.get("upload-file-content").handler({
|
|
476
|
+
driveId: "drive123",
|
|
477
|
+
driveItemId: "item456",
|
|
478
|
+
body: base64
|
|
479
|
+
});
|
|
480
|
+
const [path, options] = graphClient.graphRequest.mock.calls[0];
|
|
481
|
+
expect(path).toBe("/drives/drive123/items/item456/content");
|
|
482
|
+
expect(options.headers["Content-Type"]).toBe("application/octet-stream");
|
|
483
|
+
expect(Buffer.isBuffer(options.body) || options.body instanceof Uint8Array).toBe(true);
|
|
484
|
+
expect(Buffer.from(options.body).toString("utf-8")).toBe(original);
|
|
485
|
+
});
|
|
486
|
+
it("honors endpoints.json contentType override on binary uploads", async () => {
|
|
487
|
+
const endpoint = makeEndpoint({
|
|
488
|
+
alias: "upload-file-content",
|
|
489
|
+
method: "put",
|
|
490
|
+
path: "/drives/:driveId/items/:driveItemId/content",
|
|
491
|
+
requestFormat: "binary",
|
|
492
|
+
parameters: [
|
|
493
|
+
{ name: "driveId", type: "Path", schema: z.string() },
|
|
494
|
+
{ name: "driveItemId", type: "Path", schema: z.string() },
|
|
495
|
+
{ name: "body", type: "Body", schema: z.string() }
|
|
496
|
+
]
|
|
497
|
+
});
|
|
498
|
+
const config = makeConfig({
|
|
499
|
+
toolName: "upload-file-content",
|
|
500
|
+
method: "put",
|
|
501
|
+
pathPattern: "/drives/{drive-id}/items/{driveItem-id}/content",
|
|
502
|
+
scopes: ["Files.ReadWrite"],
|
|
503
|
+
contentType: "application/pdf"
|
|
504
|
+
});
|
|
505
|
+
mockEndpoints.push(endpoint);
|
|
506
|
+
mockEndpointsJson = [config];
|
|
507
|
+
const graphClient = createMockGraphClient([{ content: [{ type: "text", text: "{}" }] }]);
|
|
508
|
+
const server = createMockServer();
|
|
509
|
+
const { registerGraphTools } = await loadModule();
|
|
510
|
+
registerGraphTools(server, graphClient);
|
|
511
|
+
await server.tools.get("upload-file-content").handler({
|
|
512
|
+
driveId: "d",
|
|
513
|
+
driveItemId: "i",
|
|
514
|
+
body: Buffer.from("%PDF-1.4").toString("base64")
|
|
515
|
+
});
|
|
516
|
+
const [, options] = graphClient.graphRequest.mock.calls[0];
|
|
517
|
+
expect(options.headers["Content-Type"]).toBe("application/pdf");
|
|
518
|
+
});
|
|
519
|
+
});
|
|
520
|
+
describe("download-bytes", () => {
|
|
521
|
+
it("routes a relative Graph path through graphRequest", async () => {
|
|
522
|
+
mockEndpoints.length = 0;
|
|
523
|
+
mockEndpointsJson = [];
|
|
524
|
+
const graphClient = {
|
|
525
|
+
graphRequest: vi.fn().mockResolvedValue({
|
|
526
|
+
content: [
|
|
527
|
+
{
|
|
528
|
+
type: "text",
|
|
529
|
+
text: JSON.stringify({
|
|
530
|
+
contentType: "image/jpeg",
|
|
531
|
+
encoding: "base64",
|
|
532
|
+
contentBytes: "aGk="
|
|
533
|
+
})
|
|
534
|
+
}
|
|
535
|
+
]
|
|
536
|
+
})
|
|
537
|
+
};
|
|
538
|
+
const server = createMockServer();
|
|
539
|
+
const { registerGraphTools } = await loadModule();
|
|
540
|
+
registerGraphTools(server, graphClient);
|
|
541
|
+
const tool = server.tools.get("download-bytes");
|
|
542
|
+
expect(tool).toBeDefined();
|
|
543
|
+
await tool.handler({ target: "/me/photo/$value" });
|
|
544
|
+
expect(graphClient.graphRequest).toHaveBeenCalledTimes(1);
|
|
545
|
+
const [path, options] = graphClient.graphRequest.mock.calls[0];
|
|
546
|
+
expect(path).toBe("/me/photo/$value");
|
|
547
|
+
expect(options.accessToken).toBeUndefined();
|
|
548
|
+
});
|
|
549
|
+
it("rejects absolute URLs (Graph paths only)", async () => {
|
|
550
|
+
mockEndpoints.length = 0;
|
|
551
|
+
mockEndpointsJson = [];
|
|
552
|
+
const server = createMockServer();
|
|
553
|
+
const { registerGraphTools } = await loadModule();
|
|
554
|
+
registerGraphTools(server, {});
|
|
555
|
+
const tool = server.tools.get("download-bytes");
|
|
556
|
+
const result = await tool.handler({
|
|
557
|
+
target: "https://example.sharepoint.com/d/abc?temp=signed"
|
|
558
|
+
});
|
|
559
|
+
expect(result.isError).toBe(true);
|
|
560
|
+
const payload = JSON.parse(result.content[0].text);
|
|
561
|
+
expect(payload.error).toMatch(/relative Microsoft Graph path/);
|
|
562
|
+
});
|
|
563
|
+
it("rejects targets that do not start with /", async () => {
|
|
564
|
+
mockEndpoints.length = 0;
|
|
565
|
+
mockEndpointsJson = [];
|
|
566
|
+
const server = createMockServer();
|
|
567
|
+
const { registerGraphTools } = await loadModule();
|
|
568
|
+
registerGraphTools(server, {});
|
|
569
|
+
const tool = server.tools.get("download-bytes");
|
|
570
|
+
const result = await tool.handler({ target: "ftp://example.com/x" });
|
|
571
|
+
expect(result.isError).toBe(true);
|
|
572
|
+
const payload = JSON.parse(result.content[0].text);
|
|
573
|
+
expect(payload.error).toMatch(/relative Microsoft Graph path/);
|
|
574
|
+
});
|
|
575
|
+
});
|
|
576
|
+
describe("discovery mode: utility tools", () => {
|
|
577
|
+
it('search-tools surfaces download-bytes for "download" queries', async () => {
|
|
578
|
+
mockEndpoints.length = 0;
|
|
579
|
+
mockEndpointsJson = [];
|
|
580
|
+
const server = createMockServer();
|
|
581
|
+
const { registerDiscoveryTools } = await loadModule();
|
|
582
|
+
registerDiscoveryTools(server, {});
|
|
583
|
+
const result = await server.tools.get("search-tools").handler({ query: "download" });
|
|
584
|
+
const payload = JSON.parse(result.content[0].text);
|
|
585
|
+
const names = payload.tools.map((t) => t.name);
|
|
586
|
+
expect(names).toContain("download-bytes");
|
|
587
|
+
});
|
|
588
|
+
it("get-tool-schema returns the download-bytes parameter schema", async () => {
|
|
589
|
+
mockEndpoints.length = 0;
|
|
590
|
+
mockEndpointsJson = [];
|
|
591
|
+
const server = createMockServer();
|
|
592
|
+
const { registerDiscoveryTools } = await loadModule();
|
|
593
|
+
registerDiscoveryTools(server, {});
|
|
594
|
+
const result = await server.tools.get("get-tool-schema").handler({ tool_name: "download-bytes" });
|
|
595
|
+
const schema = JSON.parse(result.content[0].text);
|
|
596
|
+
expect(schema.name).toBe("download-bytes");
|
|
597
|
+
expect(schema.path).toBe("tool:download-bytes");
|
|
598
|
+
const targetParam = schema.parameters.find((p) => p.name === "target");
|
|
599
|
+
expect(targetParam).toBeDefined();
|
|
600
|
+
expect(targetParam.required).toBe(true);
|
|
601
|
+
});
|
|
602
|
+
it("execute-tool dispatches to download-bytes for a Graph path", async () => {
|
|
603
|
+
mockEndpoints.length = 0;
|
|
604
|
+
mockEndpointsJson = [];
|
|
605
|
+
const graphClient = {
|
|
606
|
+
graphRequest: vi.fn().mockResolvedValue({
|
|
607
|
+
content: [
|
|
608
|
+
{
|
|
609
|
+
type: "text",
|
|
610
|
+
text: JSON.stringify({
|
|
611
|
+
contentType: "image/png",
|
|
612
|
+
encoding: "base64",
|
|
613
|
+
contentBytes: "iVBORw0K"
|
|
614
|
+
})
|
|
615
|
+
}
|
|
616
|
+
]
|
|
617
|
+
})
|
|
618
|
+
};
|
|
619
|
+
const server = createMockServer();
|
|
620
|
+
const { registerDiscoveryTools } = await loadModule();
|
|
621
|
+
registerDiscoveryTools(server, graphClient);
|
|
622
|
+
const result = await server.tools.get("execute-tool").handler({
|
|
623
|
+
tool_name: "download-bytes",
|
|
624
|
+
parameters: { target: "/me/photo/$value" }
|
|
625
|
+
});
|
|
626
|
+
expect(result.isError).toBeFalsy();
|
|
627
|
+
expect(graphClient.graphRequest).toHaveBeenCalledTimes(1);
|
|
628
|
+
const [path] = graphClient.graphRequest.mock.calls[0];
|
|
629
|
+
expect(path).toBe("/me/photo/$value");
|
|
630
|
+
});
|
|
631
|
+
it("execute-tool reports unknown tool when name matches neither registry", async () => {
|
|
632
|
+
mockEndpoints.length = 0;
|
|
633
|
+
mockEndpointsJson = [];
|
|
634
|
+
const server = createMockServer();
|
|
635
|
+
const { registerDiscoveryTools } = await loadModule();
|
|
636
|
+
registerDiscoveryTools(server, {});
|
|
637
|
+
const result = await server.tools.get("execute-tool").handler({
|
|
638
|
+
tool_name: "no-such-tool",
|
|
639
|
+
parameters: {}
|
|
640
|
+
});
|
|
641
|
+
expect(result.isError).toBe(true);
|
|
642
|
+
const payload = JSON.parse(result.content[0].text);
|
|
643
|
+
expect(payload.error).toMatch(/not found/i);
|
|
644
|
+
});
|
|
645
|
+
});
|
|
646
|
+
describe("discovery mode: --enabled-tools filter", () => {
|
|
647
|
+
it("search-tools only surfaces Graph tools matching the regex", async () => {
|
|
648
|
+
mockEndpoints.push(
|
|
649
|
+
{
|
|
650
|
+
alias: "list-mail-messages",
|
|
651
|
+
method: "get",
|
|
652
|
+
path: "/me/messages",
|
|
653
|
+
description: "List mail",
|
|
654
|
+
parameters: []
|
|
655
|
+
},
|
|
656
|
+
{
|
|
657
|
+
alias: "list-calendar-events",
|
|
658
|
+
method: "get",
|
|
659
|
+
path: "/me/events",
|
|
660
|
+
description: "List events",
|
|
661
|
+
parameters: []
|
|
662
|
+
}
|
|
663
|
+
);
|
|
664
|
+
mockEndpointsJson = [
|
|
665
|
+
{ toolName: "list-mail-messages", method: "get", pathPattern: "/me/messages" },
|
|
666
|
+
{ toolName: "list-calendar-events", method: "get", pathPattern: "/me/events" }
|
|
667
|
+
];
|
|
668
|
+
const server = createMockServer();
|
|
669
|
+
const { registerDiscoveryTools } = await loadModule();
|
|
670
|
+
registerDiscoveryTools(server, {}, false, false, void 0, false, [], "mail");
|
|
671
|
+
const result = await server.tools.get("search-tools").handler({ limit: 50 });
|
|
672
|
+
const found = JSON.parse(result.content[0].text).tools.map((t) => t.name);
|
|
673
|
+
expect(found).toContain("list-mail-messages");
|
|
674
|
+
expect(found).not.toContain("list-calendar-events");
|
|
675
|
+
});
|
|
676
|
+
it("utility tools obey the regex too", async () => {
|
|
677
|
+
mockEndpoints.length = 0;
|
|
678
|
+
mockEndpointsJson = [];
|
|
679
|
+
const server = createMockServer();
|
|
680
|
+
const { registerDiscoveryTools } = await loadModule();
|
|
681
|
+
registerDiscoveryTools(
|
|
682
|
+
server,
|
|
683
|
+
{},
|
|
684
|
+
false,
|
|
685
|
+
false,
|
|
686
|
+
void 0,
|
|
687
|
+
false,
|
|
688
|
+
[],
|
|
689
|
+
"^download-bytes$"
|
|
690
|
+
);
|
|
691
|
+
const result = await server.tools.get("search-tools").handler({ limit: 50 });
|
|
692
|
+
const found = JSON.parse(result.content[0].text).tools.map((t) => t.name);
|
|
693
|
+
expect(found).toContain("download-bytes");
|
|
694
|
+
expect(found).not.toContain("parse-teams-url");
|
|
695
|
+
});
|
|
696
|
+
it("invalid regex pattern is ignored, all tools surface", async () => {
|
|
697
|
+
mockEndpoints.length = 0;
|
|
698
|
+
mockEndpointsJson = [];
|
|
699
|
+
const server = createMockServer();
|
|
700
|
+
const { registerDiscoveryTools } = await loadModule();
|
|
701
|
+
registerDiscoveryTools(
|
|
702
|
+
server,
|
|
703
|
+
{},
|
|
704
|
+
false,
|
|
705
|
+
false,
|
|
706
|
+
void 0,
|
|
707
|
+
false,
|
|
708
|
+
[],
|
|
709
|
+
"[invalid"
|
|
710
|
+
);
|
|
711
|
+
const result = await server.tools.get("search-tools").handler({ limit: 50 });
|
|
712
|
+
const found = JSON.parse(result.content[0].text).tools.map((t) => t.name);
|
|
713
|
+
expect(found).toContain("download-bytes");
|
|
714
|
+
expect(found).toContain("parse-teams-url");
|
|
715
|
+
});
|
|
716
|
+
});
|
|
717
|
+
describe("utility tools in read-only mode", () => {
|
|
718
|
+
it("skips utility tools whose readOnlyHint is not true", async () => {
|
|
719
|
+
mockEndpoints.length = 0;
|
|
720
|
+
mockEndpointsJson = [];
|
|
721
|
+
const server = createMockServer();
|
|
722
|
+
const { registerGraphTools } = await loadModule();
|
|
723
|
+
registerGraphTools(server, {}, true);
|
|
724
|
+
expect(server.tools.has("download-bytes")).toBe(true);
|
|
725
|
+
expect(server.tools.has("parse-teams-url")).toBe(true);
|
|
726
|
+
});
|
|
727
|
+
});
|
|
444
728
|
});
|
package/dist/endpoints.json
CHANGED
|
@@ -151,13 +151,8 @@
|
|
|
151
151
|
"pathPattern": "/me/messages/{message-id}/attachments",
|
|
152
152
|
"method": "get",
|
|
153
153
|
"toolName": "list-mail-attachments",
|
|
154
|
-
"scopes": ["Mail.Read"]
|
|
155
|
-
|
|
156
|
-
{
|
|
157
|
-
"pathPattern": "/me/messages/{message-id}/attachments/{attachment-id}",
|
|
158
|
-
"method": "get",
|
|
159
|
-
"toolName": "get-mail-attachment",
|
|
160
|
-
"scopes": ["Mail.Read"]
|
|
154
|
+
"scopes": ["Mail.Read"],
|
|
155
|
+
"llmTip": "Lists attachments on a message: id, name, contentType, size, isInline. To download the bytes, call download-bytes with target=/me/messages/{message-id}/attachments/{attachment-id}/$value (the /$value suffix returns raw bytes; the bare attachment URL embeds contentBytes in JSON which can truncate large files)."
|
|
161
156
|
},
|
|
162
157
|
{
|
|
163
158
|
"pathPattern": "/me/messages/{message-id}/attachments/{attachment-id}",
|
|
@@ -479,14 +474,6 @@
|
|
|
479
474
|
"toolName": "list-folder-files",
|
|
480
475
|
"scopes": ["Files.Read"]
|
|
481
476
|
},
|
|
482
|
-
{
|
|
483
|
-
"pathPattern": "/drives/{drive-id}/items/{driveItem-id}/content",
|
|
484
|
-
"method": "get",
|
|
485
|
-
"toolName": "download-onedrive-file-content",
|
|
486
|
-
"scopes": ["Files.Read"],
|
|
487
|
-
"returnDownloadUrl": true,
|
|
488
|
-
"llmTip": "Returns a temporary download URL, NOT the file content directly."
|
|
489
|
-
},
|
|
490
477
|
{
|
|
491
478
|
"pathPattern": "/drives/{drive-id}/items/{driveItem-id}",
|
|
492
479
|
"method": "delete",
|
|
@@ -498,7 +485,7 @@
|
|
|
498
485
|
"method": "put",
|
|
499
486
|
"toolName": "upload-file-content",
|
|
500
487
|
"scopes": ["Files.ReadWrite"],
|
|
501
|
-
"llmTip": "Max 4MB. For new files use path format: /items/root:/path/to/file.txt:/content. Overwrites existing files without warning."
|
|
488
|
+
"llmTip": "Body is a base64-encoded string of the file bytes; the server decodes it before PUT. Max 4MB inline (use create-upload-session above 4MB). For new files use path format: /items/root:/path/to/file.txt:/content. Overwrites existing files without warning."
|
|
502
489
|
},
|
|
503
490
|
{
|
|
504
491
|
"pathPattern": "/drives/{drive-id}/items/{driveItem-id}/createUploadSession",
|
|
@@ -512,7 +499,7 @@
|
|
|
512
499
|
"method": "get",
|
|
513
500
|
"toolName": "get-drive-item",
|
|
514
501
|
"scopes": ["Files.Read"],
|
|
515
|
-
"llmTip": "Gets metadata for a file or folder: name, size, lastModifiedDateTime, createdBy, webUrl, file (mimeType, hashes), folder (childCount), parentReference
|
|
502
|
+
"llmTip": "Gets metadata for a file or folder: name, size, lastModifiedDateTime, createdBy, webUrl, file (mimeType, hashes), folder (childCount), parentReference, and @microsoft.graph.downloadUrl. For the bytes, call download-bytes with target=/drives/{drive-id}/items/{driveItem-id}/content (Graph redirects to the same downloadUrl)."
|
|
516
503
|
},
|
|
517
504
|
{
|
|
518
505
|
"pathPattern": "/drives/{drive-id}/items/{driveItem-id}",
|
|
@@ -1077,22 +1064,6 @@
|
|
|
1077
1064
|
"workScopes": ["User.Read.All"],
|
|
1078
1065
|
"llmTip": "Lists users who report directly to a specific user. Use with get-user-manager to traverse the org hierarchy."
|
|
1079
1066
|
},
|
|
1080
|
-
{
|
|
1081
|
-
"pathPattern": "/me/photo/$value",
|
|
1082
|
-
"method": "get",
|
|
1083
|
-
"toolName": "get-my-profile-photo",
|
|
1084
|
-
"scopes": ["User.Read"],
|
|
1085
|
-
"returnDownloadUrl": true,
|
|
1086
|
-
"llmTip": "Returns the current user's profile photo as binary image data. The photo is typically in JPEG format."
|
|
1087
|
-
},
|
|
1088
|
-
{
|
|
1089
|
-
"pathPattern": "/users/{user-id}/photo/$value",
|
|
1090
|
-
"method": "get",
|
|
1091
|
-
"toolName": "get-user-profile-photo",
|
|
1092
|
-
"workScopes": ["User.Read.All"],
|
|
1093
|
-
"returnDownloadUrl": true,
|
|
1094
|
-
"llmTip": "Returns a specific user's profile photo as binary image data. Use the user ID or UPN (email)."
|
|
1095
|
-
},
|
|
1096
1067
|
{
|
|
1097
1068
|
"pathPattern": "/me/people",
|
|
1098
1069
|
"method": "get",
|
|
@@ -1185,15 +1156,7 @@
|
|
|
1185
1156
|
"method": "get",
|
|
1186
1157
|
"toolName": "list-chat-message-hosted-contents",
|
|
1187
1158
|
"workScopes": ["ChatMessage.Read"],
|
|
1188
|
-
"llmTip": "Lists hosted-content references (inline images, code snippets) attached to a Teams chat message. Returns
|
|
1189
|
-
},
|
|
1190
|
-
{
|
|
1191
|
-
"pathPattern": "/chats/{chat-id}/messages/{chatMessage-id}/hostedContents/{chatMessageHostedContent-id}/$value",
|
|
1192
|
-
"method": "get",
|
|
1193
|
-
"toolName": "get-chat-message-hosted-content",
|
|
1194
|
-
"workScopes": ["ChatMessage.Read"],
|
|
1195
|
-
"returnDownloadUrl": true,
|
|
1196
|
-
"llmTip": "Returns raw bytes of a hosted content item (typically image/png or image/jpeg) attached to a Teams 1:1 or group chat message. Use list-chat-message-hosted-contents to discover IDs, or extract the ID from the <img src> URL in the message body."
|
|
1159
|
+
"llmTip": "Lists hosted-content references (inline images, code snippets) attached to a Teams chat message. Returns { id, contentType } per item. To download bytes, call download-bytes with target=/chats/{chat-id}/messages/{chatMessage-id}/hostedContents/{id}/$value. IDs can also be extracted from the <img src> URL in the message body between /hostedContents/ and /$value."
|
|
1197
1160
|
},
|
|
1198
1161
|
{
|
|
1199
1162
|
"pathPattern": "/chats/{chat-id}/messages",
|
|
@@ -1261,15 +1224,7 @@
|
|
|
1261
1224
|
"method": "get",
|
|
1262
1225
|
"toolName": "list-channel-message-hosted-contents",
|
|
1263
1226
|
"workScopes": ["ChannelMessage.Read.All"],
|
|
1264
|
-
"llmTip": "Lists hosted-content references (inline images, code snippets) attached to a Teams channel message. Returns
|
|
1265
|
-
},
|
|
1266
|
-
{
|
|
1267
|
-
"pathPattern": "/teams/{team-id}/channels/{channel-id}/messages/{chatMessage-id}/hostedContents/{chatMessageHostedContent-id}/$value",
|
|
1268
|
-
"method": "get",
|
|
1269
|
-
"toolName": "get-channel-message-hosted-content",
|
|
1270
|
-
"workScopes": ["ChannelMessage.Read.All"],
|
|
1271
|
-
"returnDownloadUrl": true,
|
|
1272
|
-
"llmTip": "Returns raw bytes of a hosted content item attached to a Teams channel message. Use list-channel-message-hosted-contents to discover IDs."
|
|
1227
|
+
"llmTip": "Lists hosted-content references (inline images, code snippets) attached to a Teams channel message. Returns { id, contentType } per item. To download bytes, call download-bytes with target=/teams/{team-id}/channels/{channel-id}/messages/{chatMessage-id}/hostedContents/{id}/$value."
|
|
1273
1228
|
},
|
|
1274
1229
|
{
|
|
1275
1230
|
"pathPattern": "/teams/{team-id}/channels/{channel-id}/messages",
|
package/dist/generated/client.js
CHANGED
|
@@ -788,8 +788,8 @@ const microsoft_graph_sharePointGroupIdentity = z.object({
|
|
|
788
788
|
id: z.string().describe(
|
|
789
789
|
"Unique identifier for the identity or actor. For example, in the access reviews decisions API, this property might record the id of the principal, that is, the group, user, or application that's subject to review."
|
|
790
790
|
).nullish(),
|
|
791
|
-
principalId: z.string().nullish(),
|
|
792
|
-
title: z.string().nullish()
|
|
791
|
+
principalId: z.string().describe("The principal ID of the SharePoint group in the tenant. Read-only.").nullish(),
|
|
792
|
+
title: z.string().describe("The title of the SharePoint group. Read-only.").nullish()
|
|
793
793
|
}).passthrough();
|
|
794
794
|
const microsoft_graph_sharePointIdentity = z.object({
|
|
795
795
|
displayName: z.string().describe(
|
|
@@ -5200,14 +5200,6 @@ const endpoints = makeApi([
|
|
|
5200
5200
|
],
|
|
5201
5201
|
response: z.void()
|
|
5202
5202
|
},
|
|
5203
|
-
{
|
|
5204
|
-
method: "get",
|
|
5205
|
-
path: "/chats/:chatId/messages/:chatMessageId/hostedContents/:chatMessageHostedContentId/$value",
|
|
5206
|
-
alias: "get-chat-message-hosted-content",
|
|
5207
|
-
description: `Retrieve the list of chatMessageHostedContent objects from a message. This API only lists the hosted content objects. To get the content bytes, see get chatmessage hosted content.`,
|
|
5208
|
-
requestFormat: "json",
|
|
5209
|
-
response: z.void()
|
|
5210
|
-
},
|
|
5211
5203
|
{
|
|
5212
5204
|
method: "get",
|
|
5213
5205
|
path: "/chats/:chatId/messages/:chatMessageId/replies",
|
|
@@ -5588,21 +5580,6 @@ const endpoints = makeApi([
|
|
|
5588
5580
|
],
|
|
5589
5581
|
response: z.void()
|
|
5590
5582
|
},
|
|
5591
|
-
{
|
|
5592
|
-
method: "get",
|
|
5593
|
-
path: "/drives/:driveId/items/:driveItemId/content",
|
|
5594
|
-
alias: "download-onedrive-file-content",
|
|
5595
|
-
description: `The content stream, if the item represents a file.`,
|
|
5596
|
-
requestFormat: "json",
|
|
5597
|
-
parameters: [
|
|
5598
|
-
{
|
|
5599
|
-
name: "$format",
|
|
5600
|
-
type: "Query",
|
|
5601
|
-
schema: z.string().describe("Format of the content").optional()
|
|
5602
|
-
}
|
|
5603
|
-
],
|
|
5604
|
-
response: z.void()
|
|
5605
|
-
},
|
|
5606
5583
|
{
|
|
5607
5584
|
method: "put",
|
|
5608
5585
|
path: "/drives/:driveId/items/:driveItemId/content",
|
|
@@ -5614,7 +5591,7 @@ const endpoints = makeApi([
|
|
|
5614
5591
|
name: "body",
|
|
5615
5592
|
description: `New media content.`,
|
|
5616
5593
|
type: "Body",
|
|
5617
|
-
schema: z.
|
|
5594
|
+
schema: z.string().describe("Base64-encoded file content. The server decodes it and PUTs the raw bytes to Microsoft Graph.")
|
|
5618
5595
|
}
|
|
5619
5596
|
],
|
|
5620
5597
|
response: z.void()
|
|
@@ -9593,26 +9570,6 @@ resource.`,
|
|
|
9593
9570
|
],
|
|
9594
9571
|
response: z.void()
|
|
9595
9572
|
},
|
|
9596
|
-
{
|
|
9597
|
-
method: "get",
|
|
9598
|
-
path: "/me/messages/:messageId/attachments/:attachmentId",
|
|
9599
|
-
alias: "get-mail-attachment",
|
|
9600
|
-
description: `Read the properties, relationships, or raw contents of an attachment that is attached to a user event, message, or group post. An attachment can be one of the following types: All these types of attachments are derived from the attachment resource.`,
|
|
9601
|
-
requestFormat: "json",
|
|
9602
|
-
parameters: [
|
|
9603
|
-
{
|
|
9604
|
-
name: "$select",
|
|
9605
|
-
type: "Query",
|
|
9606
|
-
schema: z.array(z.string()).describe("Select properties to be returned").optional()
|
|
9607
|
-
},
|
|
9608
|
-
{
|
|
9609
|
-
name: "$expand",
|
|
9610
|
-
type: "Query",
|
|
9611
|
-
schema: z.array(z.string()).describe("Expand related entities").optional()
|
|
9612
|
-
}
|
|
9613
|
-
],
|
|
9614
|
-
response: z.void()
|
|
9615
|
-
},
|
|
9616
9573
|
{
|
|
9617
9574
|
method: "delete",
|
|
9618
9575
|
path: "/me/messages/:messageId/attachments/:attachmentId",
|
|
@@ -10670,17 +10627,6 @@ resource.`,
|
|
|
10670
10627
|
],
|
|
10671
10628
|
response: z.void()
|
|
10672
10629
|
},
|
|
10673
|
-
{
|
|
10674
|
-
method: "get",
|
|
10675
|
-
path: "/me/photo/$value",
|
|
10676
|
-
alias: "get-my-profile-photo",
|
|
10677
|
-
description: `Get the specified profilePhoto or its metadata (profilePhoto properties). The supported sizes of HD photos on Microsoft 365 are as follows: 48x48, 64x64, 96x96, 120x120, 240x240,
|
|
10678
|
-
360x360, 432x432, 504x504, and 648x648. Photos can be any dimension if they're stored in Microsoft Entra ID. You can get the metadata of the largest available photo or specify a size to get the metadata for that photo size.
|
|
10679
|
-
If the size you request is unavailable, you can still get a smaller size that the user has uploaded and made available.
|
|
10680
|
-
For example, if the user uploads a photo that is 504x504 pixels, all but the 648x648 size of the photo is available for download.`,
|
|
10681
|
-
requestFormat: "json",
|
|
10682
|
-
response: z.void()
|
|
10683
|
-
},
|
|
10684
10630
|
{
|
|
10685
10631
|
method: "get",
|
|
10686
10632
|
path: "/me/planner/tasks",
|
|
@@ -12879,14 +12825,6 @@ To monitor future changes, call the delta API by using the @odata.deltaLink in t
|
|
|
12879
12825
|
],
|
|
12880
12826
|
response: z.void()
|
|
12881
12827
|
},
|
|
12882
|
-
{
|
|
12883
|
-
method: "get",
|
|
12884
|
-
path: "/teams/:teamId/channels/:channelId/messages/:chatMessageId/hostedContents/:chatMessageHostedContentId/$value",
|
|
12885
|
-
alias: "get-channel-message-hosted-content",
|
|
12886
|
-
description: `Retrieve the list of chatMessageHostedContent objects from a message. This API only lists the hosted content objects. To get the content bytes, see get chatmessage hosted content.`,
|
|
12887
|
-
requestFormat: "json",
|
|
12888
|
-
response: z.void()
|
|
12889
|
-
},
|
|
12890
12828
|
{
|
|
12891
12829
|
method: "get",
|
|
12892
12830
|
path: "/teams/:teamId/channels/:channelId/messages/:chatMessageId/replies",
|
|
@@ -13286,7 +13224,7 @@ To monitor future changes, call the delta API by using the @odata.deltaLink in t
|
|
|
13286
13224
|
method: "get",
|
|
13287
13225
|
path: "/users/:userId/directReports",
|
|
13288
13226
|
alias: "list-user-direct-reports",
|
|
13289
|
-
description: `
|
|
13227
|
+
description: `Get an agentUser's direct reports. Returns the users and contacts for whom this agent user is assigned as manager.`,
|
|
13290
13228
|
requestFormat: "json",
|
|
13291
13229
|
parameters: [
|
|
13292
13230
|
{
|
|
@@ -13393,7 +13331,7 @@ To monitor future changes, call the delta API by using the @odata.deltaLink in t
|
|
|
13393
13331
|
method: "get",
|
|
13394
13332
|
path: "/users/:userId/manager",
|
|
13395
13333
|
alias: "get-user-manager",
|
|
13396
|
-
description: `Returns the user or organizational contact assigned as the
|
|
13334
|
+
description: `Returns the user or organizational contact assigned as the agentUser's manager.`,
|
|
13397
13335
|
requestFormat: "json",
|
|
13398
13336
|
parameters: [
|
|
13399
13337
|
{
|
|
@@ -13496,14 +13434,6 @@ To monitor future changes, call the delta API by using the @odata.deltaLink in t
|
|
|
13496
13434
|
],
|
|
13497
13435
|
response: z.void()
|
|
13498
13436
|
},
|
|
13499
|
-
{
|
|
13500
|
-
method: "get",
|
|
13501
|
-
path: "/users/:userId/photo/$value",
|
|
13502
|
-
alias: "get-user-profile-photo",
|
|
13503
|
-
description: `The user's profile photo. Read-only.`,
|
|
13504
|
-
requestFormat: "json",
|
|
13505
|
-
response: z.void()
|
|
13506
|
-
},
|
|
13507
13437
|
{
|
|
13508
13438
|
method: "get",
|
|
13509
13439
|
path: "/users/:userId/presence",
|
package/dist/graph-client.js
CHANGED
package/dist/graph-tools.js
CHANGED
|
@@ -8,7 +8,7 @@ import { TOOL_CATEGORIES } from "./tool-categories.js";
|
|
|
8
8
|
import { getRequestTokens } from "./request-context.js";
|
|
9
9
|
import { parseTeamsUrl } from "./lib/teams-url-parser.js";
|
|
10
10
|
import { buildBM25Index, scoreQuery, tokenize } from "./lib/bm25.js";
|
|
11
|
-
import { describeToolSchema } from "./lib/tool-schema.js";
|
|
11
|
+
import { describeToolSchema, describeUtilityToolSchema } from "./lib/tool-schema.js";
|
|
12
12
|
const __filename = fileURLToPath(import.meta.url);
|
|
13
13
|
const __dirname = path.dirname(__filename);
|
|
14
14
|
const endpointsData = JSON.parse(
|
|
@@ -34,6 +34,111 @@ function clampTopQueryParam(queryParams) {
|
|
|
34
34
|
logger.info(`Clamping $top from ${requested} to ${cap} (MS365_MCP_MAX_TOP)`);
|
|
35
35
|
queryParams["$top"] = String(cap);
|
|
36
36
|
}
|
|
37
|
+
const UTILITY_TOOLS = [
|
|
38
|
+
{
|
|
39
|
+
name: "parse-teams-url",
|
|
40
|
+
method: "POST",
|
|
41
|
+
path: "tool:parse-teams-url",
|
|
42
|
+
description: "Converts any Teams meeting URL format (short /meet/, full /meetup-join/, or recap ?threadId=) into a standard joinWebUrl. Use this before list-online-meetings when the user provides a recap or short URL.",
|
|
43
|
+
readOnlyHint: true,
|
|
44
|
+
openWorldHint: false,
|
|
45
|
+
buildSchema: () => ({
|
|
46
|
+
url: z.string().describe("Teams meeting URL in any format")
|
|
47
|
+
}),
|
|
48
|
+
execute: async (params) => {
|
|
49
|
+
const url = params.url;
|
|
50
|
+
if (typeof url !== "string") {
|
|
51
|
+
return {
|
|
52
|
+
content: [{ type: "text", text: JSON.stringify({ error: "url is required." }) }],
|
|
53
|
+
isError: true
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
try {
|
|
57
|
+
const joinWebUrl = parseTeamsUrl(url);
|
|
58
|
+
return { content: [{ type: "text", text: joinWebUrl }] };
|
|
59
|
+
} catch (error) {
|
|
60
|
+
return {
|
|
61
|
+
content: [{ type: "text", text: JSON.stringify({ error: error.message }) }],
|
|
62
|
+
isError: true
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
name: "download-bytes",
|
|
69
|
+
method: "GET",
|
|
70
|
+
path: "tool:download-bytes",
|
|
71
|
+
description: 'Download binary content from Microsoft Graph and return it as base64. Single tool for any binary read: drive file content, mail attachment, profile photo, Teams hosted content, meeting recording. Returns { contentType, encoding: "base64", contentLength, contentBytes }.',
|
|
72
|
+
readOnlyHint: true,
|
|
73
|
+
openWorldHint: true,
|
|
74
|
+
buildSchema: (ctx) => {
|
|
75
|
+
const schema = {
|
|
76
|
+
target: z.string().describe(
|
|
77
|
+
'Relative Microsoft Graph path starting with "/". Common paths: /drives/{drive-id}/items/{driveItem-id}/content (drive file content); /me/messages/{message-id}/attachments/{attachment-id}/$value (mail attachment, list-mail-attachments returns the IDs); /me/photo/$value or /users/{user-id}/photo/$value (profile photo); /chats/{chat-id}/messages/{chatMessage-id}/hostedContents/{chatMessageHostedContent-id}/$value (Teams chat hosted content, list-chat-message-hosted-contents returns the IDs); /teams/{team-id}/channels/{channel-id}/messages/{chatMessage-id}/hostedContents/{chatMessageHostedContent-id}/$value (Teams channel hosted content). For meeting recordings (often large), use get-meeting-recording-content which returns a URL for out-of-band download by the client.'
|
|
78
|
+
)
|
|
79
|
+
};
|
|
80
|
+
if (ctx.multiAccount) {
|
|
81
|
+
schema["account"] = z.string().optional().describe(
|
|
82
|
+
"Account to use when multiple Microsoft accounts are configured. Required when multiple accounts exist (see list-accounts)."
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
return schema;
|
|
86
|
+
},
|
|
87
|
+
execute: async (params, { graphClient, authManager }) => {
|
|
88
|
+
const target = params.target;
|
|
89
|
+
const accountParam = params.account;
|
|
90
|
+
if (typeof target !== "string" || target.length === 0) {
|
|
91
|
+
return {
|
|
92
|
+
content: [
|
|
93
|
+
{
|
|
94
|
+
type: "text",
|
|
95
|
+
text: JSON.stringify({ error: "target is required and must be a non-empty string." })
|
|
96
|
+
}
|
|
97
|
+
],
|
|
98
|
+
isError: true
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
if (!target.startsWith("/")) {
|
|
102
|
+
return {
|
|
103
|
+
content: [
|
|
104
|
+
{
|
|
105
|
+
type: "text",
|
|
106
|
+
text: JSON.stringify({
|
|
107
|
+
error: 'target must be a relative Microsoft Graph path starting with "/", e.g. /me/photo/$value or /drives/{drive-id}/items/{driveItem-id}/content. Absolute URLs are not accepted; if you have an @microsoft.graph.downloadUrl, use the equivalent /content or /$value path instead (Graph 302-redirects to the same bytes).'
|
|
108
|
+
})
|
|
109
|
+
}
|
|
110
|
+
],
|
|
111
|
+
isError: true
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
try {
|
|
115
|
+
let accountAccessToken;
|
|
116
|
+
if (authManager && !authManager.isOAuthModeEnabled() && !getRequestTokens()) {
|
|
117
|
+
accountAccessToken = await authManager.getTokenForAccount(accountParam);
|
|
118
|
+
}
|
|
119
|
+
return await graphClient.graphRequest(target, { accessToken: accountAccessToken });
|
|
120
|
+
} catch (error) {
|
|
121
|
+
return {
|
|
122
|
+
content: [{ type: "text", text: JSON.stringify({ error: error.message }) }],
|
|
123
|
+
isError: true
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
];
|
|
129
|
+
function registerUtilityToolWithMcp(server, utility, ctx) {
|
|
130
|
+
server.tool(
|
|
131
|
+
utility.name,
|
|
132
|
+
utility.description,
|
|
133
|
+
utility.buildSchema(ctx),
|
|
134
|
+
{
|
|
135
|
+
title: utility.name,
|
|
136
|
+
readOnlyHint: utility.readOnlyHint ?? true,
|
|
137
|
+
openWorldHint: utility.openWorldHint ?? true
|
|
138
|
+
},
|
|
139
|
+
async (params) => utility.execute(params, ctx)
|
|
140
|
+
);
|
|
141
|
+
}
|
|
37
142
|
async function executeGraphTool(tool, config, graphClient, params, authManager) {
|
|
38
143
|
logger.info(`Tool ${tool.alias} called with params: ${JSON.stringify(params)}`);
|
|
39
144
|
try {
|
|
@@ -174,7 +279,12 @@ async function executeGraphTool(tool, config, graphClient, params, authManager)
|
|
|
174
279
|
headers
|
|
175
280
|
};
|
|
176
281
|
if (options.method !== "GET" && body) {
|
|
177
|
-
if (
|
|
282
|
+
if (tool.requestFormat === "binary" && typeof body === "string") {
|
|
283
|
+
options.body = Buffer.from(body, "base64");
|
|
284
|
+
if (!config?.contentType) {
|
|
285
|
+
headers["Content-Type"] = "application/octet-stream";
|
|
286
|
+
}
|
|
287
|
+
} else if (config?.contentType === "text/html") {
|
|
178
288
|
if (typeof body === "string") {
|
|
179
289
|
options.body = body;
|
|
180
290
|
} else if (typeof body === "object" && "content" in body) {
|
|
@@ -426,36 +536,20 @@ function registerGraphTools(server, graphClient, readOnly = false, enabledToolsP
|
|
|
426
536
|
if (multiAccount) {
|
|
427
537
|
logger.info('Multi-account mode: "account" parameter injected into all tool schemas');
|
|
428
538
|
}
|
|
429
|
-
|
|
539
|
+
const utilityCtx = {
|
|
540
|
+
graphClient,
|
|
541
|
+
authManager,
|
|
542
|
+
multiAccount,
|
|
543
|
+
accountNames
|
|
544
|
+
};
|
|
545
|
+
for (const utility of UTILITY_TOOLS) {
|
|
546
|
+
if (readOnly && !utility.readOnlyHint) continue;
|
|
547
|
+
if (enabledToolsRegex && !enabledToolsRegex.test(utility.name)) continue;
|
|
430
548
|
try {
|
|
431
|
-
server
|
|
432
|
-
"parse-teams-url",
|
|
433
|
-
"Converts any Teams meeting URL format (short /meet/, full /meetup-join/, or recap ?threadId=) into a standard joinWebUrl. Use this before list-online-meetings when the user provides a recap or short URL.",
|
|
434
|
-
{
|
|
435
|
-
url: z.string().describe("Teams meeting URL in any format")
|
|
436
|
-
},
|
|
437
|
-
{
|
|
438
|
-
title: "parse-teams-url",
|
|
439
|
-
readOnlyHint: true,
|
|
440
|
-
openWorldHint: false
|
|
441
|
-
},
|
|
442
|
-
async ({ url }) => {
|
|
443
|
-
try {
|
|
444
|
-
const joinWebUrl = parseTeamsUrl(url);
|
|
445
|
-
return { content: [{ type: "text", text: joinWebUrl }] };
|
|
446
|
-
} catch (error) {
|
|
447
|
-
return {
|
|
448
|
-
content: [
|
|
449
|
-
{ type: "text", text: JSON.stringify({ error: error.message }) }
|
|
450
|
-
],
|
|
451
|
-
isError: true
|
|
452
|
-
};
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
);
|
|
549
|
+
registerUtilityToolWithMcp(server, utility, utilityCtx);
|
|
456
550
|
registeredCount++;
|
|
457
551
|
} catch (error) {
|
|
458
|
-
logger.error(`Failed to register tool
|
|
552
|
+
logger.error(`Failed to register tool ${utility.name}: ${error.message}`);
|
|
459
553
|
failedCount++;
|
|
460
554
|
}
|
|
461
555
|
}
|
|
@@ -464,7 +558,7 @@ function registerGraphTools(server, graphClient, readOnly = false, enabledToolsP
|
|
|
464
558
|
);
|
|
465
559
|
return registeredCount;
|
|
466
560
|
}
|
|
467
|
-
function buildToolsRegistry(readOnly, orgMode) {
|
|
561
|
+
function buildToolsRegistry(readOnly, orgMode, enabledToolsRegex) {
|
|
468
562
|
const toolsMap = /* @__PURE__ */ new Map();
|
|
469
563
|
for (const tool of api.endpoints) {
|
|
470
564
|
const endpointConfig = endpointsData.find((e) => e.toolName === tool.alias);
|
|
@@ -477,11 +571,14 @@ function buildToolsRegistry(readOnly, orgMode) {
|
|
|
477
571
|
continue;
|
|
478
572
|
}
|
|
479
573
|
}
|
|
574
|
+
if (enabledToolsRegex && !enabledToolsRegex.test(tool.alias)) {
|
|
575
|
+
continue;
|
|
576
|
+
}
|
|
480
577
|
toolsMap.set(tool.alias, { tool, config: endpointConfig });
|
|
481
578
|
}
|
|
482
579
|
return toolsMap;
|
|
483
580
|
}
|
|
484
|
-
function buildDiscoverySearchIndex(toolsRegistry) {
|
|
581
|
+
function buildDiscoverySearchIndex(toolsRegistry, utilityTools = []) {
|
|
485
582
|
const TIP_EXCERPT_TOKENS = 12;
|
|
486
583
|
const DESC_CAP_TOKENS = 40;
|
|
487
584
|
const docs = [];
|
|
@@ -505,6 +602,14 @@ function buildDiscoverySearchIndex(toolsRegistry) {
|
|
|
505
602
|
];
|
|
506
603
|
docs.push({ id: name, tokens });
|
|
507
604
|
}
|
|
605
|
+
for (const utility of utilityTools) {
|
|
606
|
+
const nt = tokenize(utility.name);
|
|
607
|
+
nameTokens.set(utility.name, new Set(nt));
|
|
608
|
+
const pathTokens = tokenize(utility.path);
|
|
609
|
+
const descTokens = tokenize(utility.description).slice(0, DESC_CAP_TOKENS);
|
|
610
|
+
const tokens = [...nt, ...nt, ...nt, ...nt, ...nt, ...pathTokens, ...pathTokens, ...descTokens];
|
|
611
|
+
docs.push({ id: utility.name, tokens });
|
|
612
|
+
}
|
|
508
613
|
return { bm25: buildBM25Index(docs), nameTokens };
|
|
509
614
|
}
|
|
510
615
|
function scoreDiscoveryQuery(query, index) {
|
|
@@ -530,29 +635,66 @@ function scoreDiscoveryQuery(query, index) {
|
|
|
530
635
|
ranked.sort((a, b) => b.score - a.score);
|
|
531
636
|
return ranked;
|
|
532
637
|
}
|
|
533
|
-
function registerDiscoveryTools(server, graphClient, readOnly = false, orgMode = false, authManager,
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
638
|
+
function registerDiscoveryTools(server, graphClient, readOnly = false, orgMode = false, authManager, multiAccount = false, accountNames = [], enabledTools) {
|
|
639
|
+
let enabledToolsRegex;
|
|
640
|
+
if (enabledTools) {
|
|
641
|
+
try {
|
|
642
|
+
enabledToolsRegex = new RegExp(enabledTools, "i");
|
|
643
|
+
logger.info(`Discovery mode: filtering tools with pattern ${enabledTools}`);
|
|
644
|
+
} catch (error) {
|
|
645
|
+
logger.error(
|
|
646
|
+
`Invalid --enabled-tools regex ${JSON.stringify(enabledTools)} \u2014 ignoring filter: ${error.message}`
|
|
647
|
+
);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
const toolsRegistry = buildToolsRegistry(readOnly, orgMode, enabledToolsRegex);
|
|
651
|
+
const utilityTools = UTILITY_TOOLS.filter((u) => {
|
|
652
|
+
if (readOnly && !u.readOnlyHint) return false;
|
|
653
|
+
if (enabledToolsRegex && !enabledToolsRegex.test(u.name)) return false;
|
|
654
|
+
return true;
|
|
655
|
+
});
|
|
656
|
+
const searchIndex = buildDiscoverySearchIndex(toolsRegistry, utilityTools);
|
|
657
|
+
const totalCount = toolsRegistry.size + utilityTools.length;
|
|
658
|
+
logger.info(
|
|
659
|
+
`Discovery mode: ${totalCount} tools (${toolsRegistry.size} Graph + ${utilityTools.length} utility)`
|
|
660
|
+
);
|
|
661
|
+
const utilityCtx = {
|
|
662
|
+
graphClient,
|
|
663
|
+
authManager,
|
|
664
|
+
multiAccount,
|
|
665
|
+
accountNames
|
|
666
|
+
};
|
|
667
|
+
const utilityByName = new Map(utilityTools.map((u) => [u.name, u]));
|
|
537
668
|
const categoryNames = Object.keys(TOOL_CATEGORIES).join(", ");
|
|
538
669
|
const toResultEntry = (name) => {
|
|
539
670
|
const entry = toolsRegistry.get(name);
|
|
540
|
-
if (
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
671
|
+
if (entry) {
|
|
672
|
+
const { tool, config } = entry;
|
|
673
|
+
return {
|
|
674
|
+
name,
|
|
675
|
+
method: tool.method.toUpperCase(),
|
|
676
|
+
path: tool.path,
|
|
677
|
+
description: tool.description || `${tool.method.toUpperCase()} ${tool.path}`,
|
|
678
|
+
...config?.llmTip ? { llmTip: config.llmTip } : {}
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
const utility = utilityByName.get(name);
|
|
682
|
+
if (utility) {
|
|
683
|
+
return {
|
|
684
|
+
name: utility.name,
|
|
685
|
+
method: utility.method,
|
|
686
|
+
path: utility.path,
|
|
687
|
+
description: utility.description
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
return null;
|
|
549
691
|
};
|
|
550
692
|
server.tool(
|
|
551
693
|
"search-tools",
|
|
552
|
-
`Search through ${toolsRegistry.size} Microsoft Graph API
|
|
694
|
+
`Search through ${totalCount} tools (${toolsRegistry.size} Microsoft Graph API operations + ${utilityTools.length} server utilities like download-bytes). Ranks results by BM25 over tool name, llmTip, description, and path. After picking a tool, call get-tool-schema for parameters, then execute-tool.`,
|
|
553
695
|
{
|
|
554
696
|
query: z.string().describe(
|
|
555
|
-
'Natural-language query. Tokenized and BM25-ranked. E.g. "send email", "
|
|
697
|
+
'Natural-language query. Tokenized and BM25-ranked. E.g. "send email", "download photo", "list unread messages".'
|
|
556
698
|
).optional(),
|
|
557
699
|
category: z.string().describe(`Optional pre-filter by category: ${categoryNames}`).optional(),
|
|
558
700
|
limit: z.number().describe("Maximum results (default: 10, max: 50)").optional()
|
|
@@ -571,7 +713,9 @@ function registerDiscoveryTools(server, graphClient, readOnly = false, orgMode =
|
|
|
571
713
|
const ranked = scoreDiscoveryQuery(query, searchIndex);
|
|
572
714
|
orderedNames = ranked.map((r) => r.id).filter(categoryFilter);
|
|
573
715
|
} else {
|
|
574
|
-
orderedNames = [...toolsRegistry.keys()].filter(
|
|
716
|
+
orderedNames = [...toolsRegistry.keys(), ...utilityTools.map((u) => u.name)].filter(
|
|
717
|
+
categoryFilter
|
|
718
|
+
);
|
|
575
719
|
}
|
|
576
720
|
const tools = orderedNames.slice(0, maxLimit).map(toResultEntry).filter(Boolean);
|
|
577
721
|
return {
|
|
@@ -581,7 +725,7 @@ function registerDiscoveryTools(server, graphClient, readOnly = false, orgMode =
|
|
|
581
725
|
text: JSON.stringify(
|
|
582
726
|
{
|
|
583
727
|
found: tools.length,
|
|
584
|
-
total:
|
|
728
|
+
total: totalCount,
|
|
585
729
|
tools,
|
|
586
730
|
tip: "Call get-tool-schema(tool_name) to see parameters before invoking execute-tool."
|
|
587
731
|
},
|
|
@@ -606,23 +750,30 @@ function registerDiscoveryTools(server, graphClient, readOnly = false, orgMode =
|
|
|
606
750
|
},
|
|
607
751
|
async ({ tool_name }) => {
|
|
608
752
|
const entry = toolsRegistry.get(tool_name);
|
|
609
|
-
if (
|
|
753
|
+
if (entry) {
|
|
754
|
+
const schema = describeToolSchema(entry.tool, entry.config?.llmTip);
|
|
610
755
|
return {
|
|
611
|
-
content: [
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
],
|
|
620
|
-
isError: true
|
|
756
|
+
content: [{ type: "text", text: JSON.stringify(schema, null, 2) }]
|
|
757
|
+
};
|
|
758
|
+
}
|
|
759
|
+
const utility = utilityByName.get(tool_name);
|
|
760
|
+
if (utility) {
|
|
761
|
+
const schema = describeUtilityToolSchema(utility, utilityCtx);
|
|
762
|
+
return {
|
|
763
|
+
content: [{ type: "text", text: JSON.stringify(schema, null, 2) }]
|
|
621
764
|
};
|
|
622
765
|
}
|
|
623
|
-
const schema = describeToolSchema(entry.tool, entry.config?.llmTip);
|
|
624
766
|
return {
|
|
625
|
-
content: [
|
|
767
|
+
content: [
|
|
768
|
+
{
|
|
769
|
+
type: "text",
|
|
770
|
+
text: JSON.stringify({
|
|
771
|
+
error: `Tool not found: ${tool_name}`,
|
|
772
|
+
tip: "Use search-tools to find available tools."
|
|
773
|
+
})
|
|
774
|
+
}
|
|
775
|
+
],
|
|
776
|
+
isError: true
|
|
626
777
|
};
|
|
627
778
|
}
|
|
628
779
|
);
|
|
@@ -643,25 +794,36 @@ function registerDiscoveryTools(server, graphClient, readOnly = false, orgMode =
|
|
|
643
794
|
},
|
|
644
795
|
async ({ tool_name, parameters = {} }) => {
|
|
645
796
|
const toolData = toolsRegistry.get(tool_name);
|
|
646
|
-
if (
|
|
647
|
-
return
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
})
|
|
655
|
-
}
|
|
656
|
-
],
|
|
657
|
-
isError: true
|
|
658
|
-
};
|
|
797
|
+
if (toolData) {
|
|
798
|
+
return executeGraphTool(
|
|
799
|
+
toolData.tool,
|
|
800
|
+
toolData.config,
|
|
801
|
+
graphClient,
|
|
802
|
+
parameters,
|
|
803
|
+
authManager
|
|
804
|
+
);
|
|
659
805
|
}
|
|
660
|
-
|
|
806
|
+
const utility = utilityByName.get(tool_name);
|
|
807
|
+
if (utility) {
|
|
808
|
+
return utility.execute(parameters, utilityCtx);
|
|
809
|
+
}
|
|
810
|
+
return {
|
|
811
|
+
content: [
|
|
812
|
+
{
|
|
813
|
+
type: "text",
|
|
814
|
+
text: JSON.stringify({
|
|
815
|
+
error: `Tool not found: ${tool_name}`,
|
|
816
|
+
tip: "Use search-tools to find available tools."
|
|
817
|
+
})
|
|
818
|
+
}
|
|
819
|
+
],
|
|
820
|
+
isError: true
|
|
821
|
+
};
|
|
661
822
|
}
|
|
662
823
|
);
|
|
663
824
|
}
|
|
664
825
|
export {
|
|
826
|
+
UTILITY_TOOLS,
|
|
665
827
|
buildDiscoverySearchIndex,
|
|
666
828
|
buildToolsRegistry,
|
|
667
829
|
registerDiscoveryTools,
|
package/dist/lib/tool-schema.js
CHANGED
|
@@ -30,6 +30,29 @@ function describeToolSchema(tool, llmTip) {
|
|
|
30
30
|
parameters: params
|
|
31
31
|
};
|
|
32
32
|
}
|
|
33
|
+
function describeUtilityToolSchema(utility, ctx) {
|
|
34
|
+
const schemaMap = utility.buildSchema(ctx);
|
|
35
|
+
const params = Object.entries(schemaMap).map(([name, zodSchema]) => {
|
|
36
|
+
const { inner, optional } = unwrapOptional(zodSchema);
|
|
37
|
+
const jsonSchema = zodToJsonSchema(inner, { target: "jsonSchema7", $refStrategy: "none" });
|
|
38
|
+
const { $schema: _s, ...schema } = jsonSchema;
|
|
39
|
+
return {
|
|
40
|
+
name,
|
|
41
|
+
in: "Query",
|
|
42
|
+
required: !optional,
|
|
43
|
+
description: zodSchema.description,
|
|
44
|
+
schema
|
|
45
|
+
};
|
|
46
|
+
});
|
|
47
|
+
return {
|
|
48
|
+
name: utility.name,
|
|
49
|
+
method: utility.method,
|
|
50
|
+
path: utility.path,
|
|
51
|
+
description: utility.description,
|
|
52
|
+
parameters: params
|
|
53
|
+
};
|
|
54
|
+
}
|
|
33
55
|
export {
|
|
34
|
-
describeToolSchema
|
|
56
|
+
describeToolSchema,
|
|
57
|
+
describeUtilityToolSchema
|
|
35
58
|
};
|
package/dist/mcp-instructions.js
CHANGED
|
@@ -5,7 +5,8 @@ function buildGeneralMcpInstructions(opts) {
|
|
|
5
5
|
"Mail and message $search uses KQL; the $search query parameter value must be double-quoted per Graph (see search-query-parameter in Microsoft Graph docs).",
|
|
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
|
-
"Teams chat and channel messages: prefer HTML contentType in the body; plain text is often mangled by Graph."
|
|
8
|
+
"Teams chat and channel messages: prefer HTML contentType in the body; plain text is often mangled by Graph.",
|
|
9
|
+
"Files / binary content: use download-bytes for any binary read (drive file content, mail attachments, profile photos, Teams hosted content, meeting recordings); pass it a Graph path or an absolute @microsoft.graph.downloadUrl from a metadata response. For uploads, upload-file-content takes a base64 string body up to 4MB; use create-upload-session above that."
|
|
9
10
|
];
|
|
10
11
|
if (opts.readOnly) parts.push("This server is read-only; write operations are disabled.");
|
|
11
12
|
if (opts.multiAccount)
|
package/dist/server.js
CHANGED
package/dist/tool-categories.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const TOOL_CATEGORIES = {
|
|
2
2
|
mail: {
|
|
3
3
|
name: "mail",
|
|
4
|
-
pattern: /mail|attachment|draft/i,
|
|
4
|
+
pattern: /mail|attachment|draft|download-bytes/i,
|
|
5
5
|
description: "Email operations (read, send, manage folders, attachments)"
|
|
6
6
|
},
|
|
7
7
|
calendar: {
|
|
@@ -16,12 +16,12 @@ const TOOL_CATEGORIES = {
|
|
|
16
16
|
},
|
|
17
17
|
personal: {
|
|
18
18
|
name: "personal",
|
|
19
|
-
pattern: /mail|calendar|drive|contact|todo|onenote|attachment|draft|event|file|folder|search|query/i,
|
|
19
|
+
pattern: /mail|calendar|drive|contact|todo|onenote|attachment|draft|event|file|folder|search|query|download-bytes|parse-teams-url/i,
|
|
20
20
|
description: "Personal productivity tools (mail, calendar, files, contacts, tasks, notes, search)"
|
|
21
21
|
},
|
|
22
22
|
work: {
|
|
23
23
|
name: "work",
|
|
24
|
-
pattern: /team|channel|chat|sharepoint|planner|site|list|shared|search|query/i,
|
|
24
|
+
pattern: /team|channel|chat|sharepoint|planner|site|list|shared|search|query|download-bytes/i,
|
|
25
25
|
description: "Organization/work tools (Teams, SharePoint, shared mailboxes, search)",
|
|
26
26
|
requiresOrgMode: true
|
|
27
27
|
},
|
|
@@ -52,7 +52,7 @@ const TOOL_CATEGORIES = {
|
|
|
52
52
|
},
|
|
53
53
|
users: {
|
|
54
54
|
name: "users",
|
|
55
|
-
pattern: /user|list-users/i,
|
|
55
|
+
pattern: /user|list-users|download-bytes/i,
|
|
56
56
|
description: "User directory access",
|
|
57
57
|
requiresOrgMode: true
|
|
58
58
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@softeria/ms-365-mcp-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.98.0",
|
|
4
4
|
"description": " A Model Context Protocol (MCP) server for interacting with Microsoft 365 and Office services through the Graph API",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
package/src/endpoints.json
CHANGED
|
@@ -151,13 +151,8 @@
|
|
|
151
151
|
"pathPattern": "/me/messages/{message-id}/attachments",
|
|
152
152
|
"method": "get",
|
|
153
153
|
"toolName": "list-mail-attachments",
|
|
154
|
-
"scopes": ["Mail.Read"]
|
|
155
|
-
|
|
156
|
-
{
|
|
157
|
-
"pathPattern": "/me/messages/{message-id}/attachments/{attachment-id}",
|
|
158
|
-
"method": "get",
|
|
159
|
-
"toolName": "get-mail-attachment",
|
|
160
|
-
"scopes": ["Mail.Read"]
|
|
154
|
+
"scopes": ["Mail.Read"],
|
|
155
|
+
"llmTip": "Lists attachments on a message: id, name, contentType, size, isInline. To download the bytes, call download-bytes with target=/me/messages/{message-id}/attachments/{attachment-id}/$value (the /$value suffix returns raw bytes; the bare attachment URL embeds contentBytes in JSON which can truncate large files)."
|
|
161
156
|
},
|
|
162
157
|
{
|
|
163
158
|
"pathPattern": "/me/messages/{message-id}/attachments/{attachment-id}",
|
|
@@ -479,14 +474,6 @@
|
|
|
479
474
|
"toolName": "list-folder-files",
|
|
480
475
|
"scopes": ["Files.Read"]
|
|
481
476
|
},
|
|
482
|
-
{
|
|
483
|
-
"pathPattern": "/drives/{drive-id}/items/{driveItem-id}/content",
|
|
484
|
-
"method": "get",
|
|
485
|
-
"toolName": "download-onedrive-file-content",
|
|
486
|
-
"scopes": ["Files.Read"],
|
|
487
|
-
"returnDownloadUrl": true,
|
|
488
|
-
"llmTip": "Returns a temporary download URL, NOT the file content directly."
|
|
489
|
-
},
|
|
490
477
|
{
|
|
491
478
|
"pathPattern": "/drives/{drive-id}/items/{driveItem-id}",
|
|
492
479
|
"method": "delete",
|
|
@@ -498,7 +485,7 @@
|
|
|
498
485
|
"method": "put",
|
|
499
486
|
"toolName": "upload-file-content",
|
|
500
487
|
"scopes": ["Files.ReadWrite"],
|
|
501
|
-
"llmTip": "Max 4MB. For new files use path format: /items/root:/path/to/file.txt:/content. Overwrites existing files without warning."
|
|
488
|
+
"llmTip": "Body is a base64-encoded string of the file bytes; the server decodes it before PUT. Max 4MB inline (use create-upload-session above 4MB). For new files use path format: /items/root:/path/to/file.txt:/content. Overwrites existing files without warning."
|
|
502
489
|
},
|
|
503
490
|
{
|
|
504
491
|
"pathPattern": "/drives/{drive-id}/items/{driveItem-id}/createUploadSession",
|
|
@@ -512,7 +499,7 @@
|
|
|
512
499
|
"method": "get",
|
|
513
500
|
"toolName": "get-drive-item",
|
|
514
501
|
"scopes": ["Files.Read"],
|
|
515
|
-
"llmTip": "Gets metadata for a file or folder: name, size, lastModifiedDateTime, createdBy, webUrl, file (mimeType, hashes), folder (childCount), parentReference
|
|
502
|
+
"llmTip": "Gets metadata for a file or folder: name, size, lastModifiedDateTime, createdBy, webUrl, file (mimeType, hashes), folder (childCount), parentReference, and @microsoft.graph.downloadUrl. For the bytes, call download-bytes with target=/drives/{drive-id}/items/{driveItem-id}/content (Graph redirects to the same downloadUrl)."
|
|
516
503
|
},
|
|
517
504
|
{
|
|
518
505
|
"pathPattern": "/drives/{drive-id}/items/{driveItem-id}",
|
|
@@ -1077,22 +1064,6 @@
|
|
|
1077
1064
|
"workScopes": ["User.Read.All"],
|
|
1078
1065
|
"llmTip": "Lists users who report directly to a specific user. Use with get-user-manager to traverse the org hierarchy."
|
|
1079
1066
|
},
|
|
1080
|
-
{
|
|
1081
|
-
"pathPattern": "/me/photo/$value",
|
|
1082
|
-
"method": "get",
|
|
1083
|
-
"toolName": "get-my-profile-photo",
|
|
1084
|
-
"scopes": ["User.Read"],
|
|
1085
|
-
"returnDownloadUrl": true,
|
|
1086
|
-
"llmTip": "Returns the current user's profile photo as binary image data. The photo is typically in JPEG format."
|
|
1087
|
-
},
|
|
1088
|
-
{
|
|
1089
|
-
"pathPattern": "/users/{user-id}/photo/$value",
|
|
1090
|
-
"method": "get",
|
|
1091
|
-
"toolName": "get-user-profile-photo",
|
|
1092
|
-
"workScopes": ["User.Read.All"],
|
|
1093
|
-
"returnDownloadUrl": true,
|
|
1094
|
-
"llmTip": "Returns a specific user's profile photo as binary image data. Use the user ID or UPN (email)."
|
|
1095
|
-
},
|
|
1096
1067
|
{
|
|
1097
1068
|
"pathPattern": "/me/people",
|
|
1098
1069
|
"method": "get",
|
|
@@ -1185,15 +1156,7 @@
|
|
|
1185
1156
|
"method": "get",
|
|
1186
1157
|
"toolName": "list-chat-message-hosted-contents",
|
|
1187
1158
|
"workScopes": ["ChatMessage.Read"],
|
|
1188
|
-
"llmTip": "Lists hosted-content references (inline images, code snippets) attached to a Teams chat message. Returns
|
|
1189
|
-
},
|
|
1190
|
-
{
|
|
1191
|
-
"pathPattern": "/chats/{chat-id}/messages/{chatMessage-id}/hostedContents/{chatMessageHostedContent-id}/$value",
|
|
1192
|
-
"method": "get",
|
|
1193
|
-
"toolName": "get-chat-message-hosted-content",
|
|
1194
|
-
"workScopes": ["ChatMessage.Read"],
|
|
1195
|
-
"returnDownloadUrl": true,
|
|
1196
|
-
"llmTip": "Returns raw bytes of a hosted content item (typically image/png or image/jpeg) attached to a Teams 1:1 or group chat message. Use list-chat-message-hosted-contents to discover IDs, or extract the ID from the <img src> URL in the message body."
|
|
1159
|
+
"llmTip": "Lists hosted-content references (inline images, code snippets) attached to a Teams chat message. Returns { id, contentType } per item. To download bytes, call download-bytes with target=/chats/{chat-id}/messages/{chatMessage-id}/hostedContents/{id}/$value. IDs can also be extracted from the <img src> URL in the message body between /hostedContents/ and /$value."
|
|
1197
1160
|
},
|
|
1198
1161
|
{
|
|
1199
1162
|
"pathPattern": "/chats/{chat-id}/messages",
|
|
@@ -1261,15 +1224,7 @@
|
|
|
1261
1224
|
"method": "get",
|
|
1262
1225
|
"toolName": "list-channel-message-hosted-contents",
|
|
1263
1226
|
"workScopes": ["ChannelMessage.Read.All"],
|
|
1264
|
-
"llmTip": "Lists hosted-content references (inline images, code snippets) attached to a Teams channel message. Returns
|
|
1265
|
-
},
|
|
1266
|
-
{
|
|
1267
|
-
"pathPattern": "/teams/{team-id}/channels/{channel-id}/messages/{chatMessage-id}/hostedContents/{chatMessageHostedContent-id}/$value",
|
|
1268
|
-
"method": "get",
|
|
1269
|
-
"toolName": "get-channel-message-hosted-content",
|
|
1270
|
-
"workScopes": ["ChannelMessage.Read.All"],
|
|
1271
|
-
"returnDownloadUrl": true,
|
|
1272
|
-
"llmTip": "Returns raw bytes of a hosted content item attached to a Teams channel message. Use list-channel-message-hosted-contents to discover IDs."
|
|
1227
|
+
"llmTip": "Lists hosted-content references (inline images, code snippets) attached to a Teams channel message. Returns { id, contentType } per item. To download bytes, call download-bytes with target=/teams/{team-id}/channels/{channel-id}/messages/{chatMessage-id}/hostedContents/{id}/$value."
|
|
1273
1228
|
},
|
|
1274
1229
|
{
|
|
1275
1230
|
"pathPattern": "/teams/{team-id}/channels/{channel-id}/messages",
|
|
@@ -14,7 +14,7 @@ export type Endpoint = {
|
|
|
14
14
|
path: string;
|
|
15
15
|
alias: string;
|
|
16
16
|
description?: string;
|
|
17
|
-
requestFormat: 'json';
|
|
17
|
+
requestFormat: 'json' | 'binary' | 'form-data' | 'form-url' | 'text';
|
|
18
18
|
parameters?: Parameter[];
|
|
19
19
|
response: z.ZodType<any>;
|
|
20
20
|
errors?: Array<{
|