@matimo/microsoft 0.1.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.
@@ -0,0 +1,96 @@
1
+ /**
2
+ * ms_create_document — PUT /drives/{drive-id}/items/{parent-item-id}:/{filename}:/content
3
+ * https://learn.microsoft.com/en-us/graph/api/driveitem-put-content
4
+ *
5
+ * Uses the "simple upload" by-path addressing syntax. Graph caps this endpoint at
6
+ * 4 MB; larger files require a resumable upload session, which is out of scope here
7
+ * and is rejected with a clear validation error rather than silently truncating.
8
+ */
9
+ import { MatimoError, ErrorCode } from '@matimo/core';
10
+ import { getAccessToken, requireParams, graphRequest, type ToolContext } from '../graph-client';
11
+
12
+ const VALID_ENCODINGS = ['text', 'base64'];
13
+ const VALID_CONFLICT_BEHAVIOURS = ['replace', 'rename', 'fail'];
14
+ const DEFAULT_PARENT_ITEM_ID = 'root';
15
+ const MAX_UPLOAD_BYTES = 4 * 1024 * 1024;
16
+
17
+ interface UploadedItem {
18
+ id?: string;
19
+ name?: string;
20
+ webUrl?: string;
21
+ size?: number;
22
+ }
23
+
24
+ export default async function execute(
25
+ params: Record<string, unknown>,
26
+ context?: ToolContext
27
+ ): Promise<unknown> {
28
+ requireParams(params, ['drive_id', 'filename', 'content'], 'ms_create_document');
29
+
30
+ const driveId = String(params.drive_id);
31
+ const parentItemId =
32
+ typeof params.parent_item_id === 'string' && params.parent_item_id
33
+ ? params.parent_item_id
34
+ : DEFAULT_PARENT_ITEM_ID;
35
+ const filename = String(params.filename);
36
+
37
+ const encoding = params.content_encoding === undefined ? 'text' : String(params.content_encoding);
38
+ if (!VALID_ENCODINGS.includes(encoding)) {
39
+ throw new MatimoError(
40
+ `ms_create_document: 'content_encoding' must be one of ${VALID_ENCODINGS.join(', ')} (received '${encoding}')`,
41
+ ErrorCode.VALIDATION_FAILED,
42
+ { content_encoding: params.content_encoding }
43
+ );
44
+ }
45
+
46
+ const conflictBehaviour =
47
+ params.conflict_behaviour === undefined ? 'replace' : String(params.conflict_behaviour);
48
+ if (!VALID_CONFLICT_BEHAVIOURS.includes(conflictBehaviour)) {
49
+ throw new MatimoError(
50
+ `ms_create_document: 'conflict_behaviour' must be one of ${VALID_CONFLICT_BEHAVIOURS.join(', ')} (received '${conflictBehaviour}')`,
51
+ ErrorCode.VALIDATION_FAILED,
52
+ { conflict_behaviour: params.conflict_behaviour }
53
+ );
54
+ }
55
+
56
+ const buffer =
57
+ encoding === 'base64'
58
+ ? Buffer.from(String(params.content), 'base64')
59
+ : Buffer.from(String(params.content), 'utf-8');
60
+
61
+ if (buffer.byteLength > MAX_UPLOAD_BYTES) {
62
+ throw new MatimoError(
63
+ `ms_create_document: content is ${buffer.byteLength} bytes, exceeding the ` +
64
+ `${MAX_UPLOAD_BYTES}-byte limit of the simple-upload endpoint. Files this large ` +
65
+ 'require a resumable upload session, which this tool does not implement.',
66
+ ErrorCode.VALIDATION_FAILED,
67
+ { sizeBytes: buffer.byteLength, maxBytes: MAX_UPLOAD_BYTES }
68
+ );
69
+ }
70
+
71
+ const token = getAccessToken(context);
72
+
73
+ // By-path addressing uses literal colons as delimiters — only the path SEGMENTS
74
+ // (drive id, parent item id, filename) are percent-encoded, not the colons.
75
+ const path =
76
+ `/drives/${encodeURIComponent(driveId)}/items/${encodeURIComponent(parentItemId)}` +
77
+ `:/${encodeURIComponent(filename)}:/content`;
78
+
79
+ const item = await graphRequest<UploadedItem>({
80
+ method: 'PUT',
81
+ path,
82
+ token,
83
+ resourceType: 'Drive folder',
84
+ query: { '@microsoft.graph.conflictBehavior': conflictBehaviour },
85
+ body: buffer,
86
+ headers: { 'Content-Type': 'application/octet-stream' },
87
+ });
88
+
89
+ return {
90
+ success: true,
91
+ item_id: item?.id ?? '',
92
+ name: item?.name ?? filename,
93
+ web_url: item?.webUrl ?? '',
94
+ size_bytes: item?.size ?? buffer.byteLength,
95
+ };
96
+ }
@@ -0,0 +1,88 @@
1
+ name: ms_get_email
2
+ description: |
3
+ List messages in the signed-in user's mailbox (GET /me/messages), with optional
4
+ OData filtering, free-text search, and folder scoping. Returns a clean, agent-friendly
5
+ summary of each message rather than the full raw Graph payload.
6
+ version: '1.0.0'
7
+ status: approved
8
+ risk: low
9
+
10
+ parameters:
11
+ top:
12
+ type: number
13
+ description: Maximum number of messages to return (1-50)
14
+ required: false
15
+ default: 10
16
+
17
+ filter:
18
+ type: string
19
+ description: >-
20
+ Raw OData $filter expression, e.g. "isRead eq false" or
21
+ "from/emailAddress/address eq 'alerts@contoso.com'"
22
+ required: false
23
+
24
+ search:
25
+ type: string
26
+ description: Free-text $search expression, e.g. '"quarterly report"'
27
+ required: false
28
+
29
+ folder_id:
30
+ type: string
31
+ description: >-
32
+ ID (or well-known name such as "inbox", "sentitems", "drafts") of the mail
33
+ folder to list messages from. Defaults to the entire mailbox when omitted.
34
+ required: false
35
+
36
+ execution:
37
+ type: function
38
+ code: ms_get_email.ts
39
+ timeout: 20000
40
+
41
+ authentication:
42
+ type: oauth2
43
+ provider: microsoft
44
+ scopes:
45
+ - https://graph.microsoft.com/Mail.Read
46
+
47
+ output_schema:
48
+ type: object
49
+ properties:
50
+ success:
51
+ type: boolean
52
+ messages:
53
+ type: array
54
+ items:
55
+ type: object
56
+ properties:
57
+ id:
58
+ type: string
59
+ subject:
60
+ type: string
61
+ from:
62
+ type: string
63
+ description: Display name and/or email address of the sender
64
+ received_at:
65
+ type: string
66
+ is_read:
67
+ type: boolean
68
+ body_preview:
69
+ type: string
70
+ has_attachments:
71
+ type: boolean
72
+
73
+ error_handling:
74
+ retry: 2
75
+ backoff_type: exponential
76
+ initial_delay_ms: 500
77
+
78
+ tags: [microsoft, graph, mail, outlook, read]
79
+
80
+ examples:
81
+ - name: List the 5 most recent unread messages
82
+ params:
83
+ top: 5
84
+ filter: 'isRead eq false'
85
+ - name: Search the inbox for messages mentioning "invoice"
86
+ params:
87
+ folder_id: inbox
88
+ search: '"invoice"'
@@ -0,0 +1,94 @@
1
+ /**
2
+ * ms_get_email — GET /me/messages
3
+ * https://learn.microsoft.com/en-us/graph/api/user-list-messages
4
+ */
5
+ import { MatimoError, ErrorCode } from '@matimo/core';
6
+ import { getAccessToken, requireParams, graphRequest, type ToolContext } from '../graph-client';
7
+
8
+ const DEFAULT_TOP = 10;
9
+ const MAX_TOP = 50;
10
+
11
+ interface EmailAddress {
12
+ name?: string;
13
+ address?: string;
14
+ }
15
+
16
+ interface MessageRecipient {
17
+ emailAddress?: EmailAddress;
18
+ }
19
+
20
+ interface GraphMessage {
21
+ id?: string;
22
+ subject?: string;
23
+ from?: MessageRecipient;
24
+ receivedDateTime?: string;
25
+ isRead?: boolean;
26
+ bodyPreview?: string;
27
+ hasAttachments?: boolean;
28
+ }
29
+
30
+ interface MessagesResponse {
31
+ value?: GraphMessage[];
32
+ }
33
+
34
+ function formatSender(message: GraphMessage): string {
35
+ const address = message.from?.emailAddress;
36
+ if (!address) return '';
37
+ if (address.name && address.address) return `${address.name} <${address.address}>`;
38
+ return address.name ?? address.address ?? '';
39
+ }
40
+
41
+ export default async function execute(
42
+ params: Record<string, unknown>,
43
+ context?: ToolContext
44
+ ): Promise<unknown> {
45
+ requireParams(params, [], 'ms_get_email');
46
+
47
+ const top = params.top === undefined ? DEFAULT_TOP : Number(params.top);
48
+ if (!Number.isFinite(top) || top < 1 || top > MAX_TOP) {
49
+ throw new MatimoError(
50
+ `ms_get_email: 'top' must be a number between 1 and ${MAX_TOP} (received ${String(params.top)})`,
51
+ ErrorCode.VALIDATION_FAILED,
52
+ { top: params.top }
53
+ );
54
+ }
55
+
56
+ const folderId = typeof params.folder_id === 'string' && params.folder_id ? params.folder_id : undefined;
57
+ const filter = typeof params.filter === 'string' && params.filter ? params.filter : undefined;
58
+ const search = typeof params.search === 'string' && params.search ? params.search : undefined;
59
+
60
+ const token = getAccessToken(context);
61
+
62
+ const path = folderId
63
+ ? `/me/mailFolders/${encodeURIComponent(folderId)}/messages`
64
+ : '/me/messages';
65
+
66
+ const data = await graphRequest<MessagesResponse>({
67
+ method: 'GET',
68
+ path,
69
+ token,
70
+ resourceType: 'Mail folder',
71
+ headers: search ? { 'ConsistencyLevel': 'eventual' } : undefined,
72
+ query: {
73
+ $top: top,
74
+ $select: 'id,subject,from,receivedDateTime,isRead,bodyPreview,hasAttachments',
75
+ ...(filter ? { $filter: filter } : {}),
76
+ ...(search ? { $search: search } : {}),
77
+ },
78
+ });
79
+
80
+ const messages = (data?.value ?? []).map((message) => ({
81
+ id: message.id ?? '',
82
+ subject: message.subject ?? '',
83
+ from: formatSender(message),
84
+ received_at: message.receivedDateTime ?? '',
85
+ is_read: message.isRead ?? false,
86
+ body_preview: message.bodyPreview ?? '',
87
+ has_attachments: message.hasAttachments ?? false,
88
+ }));
89
+
90
+ return {
91
+ success: true,
92
+ messages,
93
+ };
94
+ }
@@ -0,0 +1,81 @@
1
+ name: ms_list_files
2
+ description: |
3
+ List the children of a folder in OneDrive or a SharePoint document library
4
+ (GET /drives/{drive_id}/items/{item_id}/children). Defaults to listing the root
5
+ folder of the drive. Returns a flat list of files and subfolders with basic metadata.
6
+ version: '1.0.0'
7
+ status: approved
8
+ risk: low
9
+
10
+ parameters:
11
+ drive_id:
12
+ type: string
13
+ description: ID of the drive (OneDrive or SharePoint document library) to list
14
+ required: true
15
+
16
+ item_id:
17
+ type: string
18
+ description: ID of the folder item whose children should be listed
19
+ required: false
20
+ default: root
21
+
22
+ top:
23
+ type: number
24
+ description: Maximum number of items to return (1-100)
25
+ required: false
26
+ default: 20
27
+
28
+ execution:
29
+ type: function
30
+ code: ms_list_files.ts
31
+ timeout: 20000
32
+
33
+ authentication:
34
+ type: oauth2
35
+ provider: microsoft
36
+ scopes:
37
+ - https://graph.microsoft.com/Files.Read.All
38
+
39
+ output_schema:
40
+ type: object
41
+ properties:
42
+ success:
43
+ type: boolean
44
+ items:
45
+ type: array
46
+ items:
47
+ type: object
48
+ properties:
49
+ id:
50
+ type: string
51
+ name:
52
+ type: string
53
+ type:
54
+ type: string
55
+ description: "'file' or 'folder'"
56
+ size_bytes:
57
+ type: number
58
+ last_modified:
59
+ type: string
60
+ mime_type:
61
+ type: string
62
+ description: Present only for files (folders have no MIME type)
63
+ web_url:
64
+ type: string
65
+
66
+ error_handling:
67
+ retry: 2
68
+ backoff_type: exponential
69
+ initial_delay_ms: 500
70
+
71
+ tags: [microsoft, graph, files, onedrive, sharepoint, list]
72
+
73
+ examples:
74
+ - name: List the root of a OneDrive
75
+ params:
76
+ drive_id: 'b!xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
77
+ - name: List a specific folder, capped at 50 items
78
+ params:
79
+ drive_id: 'b!xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
80
+ item_id: '01ABCXYZ7654321'
81
+ top: 50
@@ -0,0 +1,71 @@
1
+ /**
2
+ * ms_list_files — GET /drives/{drive_id}/items/{item_id}/children
3
+ * https://learn.microsoft.com/en-us/graph/api/driveitem-list-children
4
+ */
5
+ import { MatimoError, ErrorCode } from '@matimo/core';
6
+ import { getAccessToken, requireParams, graphRequest, type ToolContext } from '../graph-client';
7
+
8
+ const DEFAULT_ITEM_ID = 'root';
9
+ const DEFAULT_TOP = 20;
10
+ const MAX_TOP = 100;
11
+
12
+ interface DriveItem {
13
+ id?: string;
14
+ name?: string;
15
+ size?: number;
16
+ lastModifiedDateTime?: string;
17
+ webUrl?: string;
18
+ file?: { mimeType?: string };
19
+ folder?: unknown;
20
+ }
21
+
22
+ interface ChildrenResponse {
23
+ value?: DriveItem[];
24
+ }
25
+
26
+ export default async function execute(
27
+ params: Record<string, unknown>,
28
+ context?: ToolContext
29
+ ): Promise<unknown> {
30
+ requireParams(params, ['drive_id'], 'ms_list_files');
31
+
32
+ const driveId = String(params.drive_id);
33
+ const itemId = typeof params.item_id === 'string' && params.item_id ? params.item_id : DEFAULT_ITEM_ID;
34
+
35
+ const top = params.top === undefined ? DEFAULT_TOP : Number(params.top);
36
+ if (!Number.isFinite(top) || top < 1 || top > MAX_TOP) {
37
+ throw new MatimoError(
38
+ `ms_list_files: 'top' must be a number between 1 and ${MAX_TOP} (received ${String(params.top)})`,
39
+ ErrorCode.VALIDATION_FAILED,
40
+ { top: params.top }
41
+ );
42
+ }
43
+
44
+ const token = getAccessToken(context);
45
+
46
+ const data = await graphRequest<ChildrenResponse>({
47
+ method: 'GET',
48
+ path: `/drives/${encodeURIComponent(driveId)}/items/${encodeURIComponent(itemId)}/children`,
49
+ token,
50
+ resourceType: 'Drive folder',
51
+ query: {
52
+ $top: top,
53
+ $select: 'id,name,size,lastModifiedDateTime,webUrl,file,folder',
54
+ },
55
+ });
56
+
57
+ const items = (data?.value ?? []).map((item) => ({
58
+ id: item.id ?? '',
59
+ name: item.name ?? '',
60
+ type: item.folder ? 'folder' : 'file',
61
+ size_bytes: item.size ?? 0,
62
+ last_modified: item.lastModifiedDateTime ?? '',
63
+ ...(item.file?.mimeType ? { mime_type: item.file.mimeType } : {}),
64
+ web_url: item.webUrl ?? '',
65
+ }));
66
+
67
+ return {
68
+ success: true,
69
+ items,
70
+ };
71
+ }
@@ -0,0 +1,92 @@
1
+ name: ms_publish_to_sharepoint
2
+ description: |
3
+ Create a SharePoint site page (POST /sites/{site_id}/pages) with a single text web
4
+ part containing the supplied content, then optionally publish it
5
+ (POST /sites/{site_id}/pages/{page_id}/microsoft.graph.sitePage/publish). Publishing
6
+ makes the page visible to everyone with site access — high-risk, requires approval.
7
+ version: '1.0.0'
8
+ status: approved
9
+ risk: high
10
+ requires_approval: true
11
+
12
+ parameters:
13
+ site_id:
14
+ type: string
15
+ description: ID of the SharePoint site to publish the page to
16
+ required: true
17
+
18
+ title:
19
+ type: string
20
+ description: Title of the page. Also used to derive the page's file name.
21
+ required: true
22
+
23
+ content:
24
+ type: string
25
+ description: Body content for the page's single text web part
26
+ required: true
27
+
28
+ content_type:
29
+ type: string
30
+ description: >-
31
+ Whether `content` is HTML or plain text. Plain text is escaped and wrapped in
32
+ a paragraph before being placed in the web part's HTML container, since
33
+ SharePoint site pages always store web part content as HTML.
34
+ required: false
35
+ default: html
36
+ enum: [html, text]
37
+
38
+ publish:
39
+ type: boolean
40
+ description: When true (the default), publish the page immediately after creating it
41
+ required: false
42
+ default: true
43
+
44
+ execution:
45
+ type: function
46
+ code: ms_publish_to_sharepoint.ts
47
+ timeout: 30000
48
+
49
+ authentication:
50
+ type: oauth2
51
+ provider: microsoft
52
+ scopes:
53
+ - https://graph.microsoft.com/Sites.Manage.All
54
+
55
+ output_schema:
56
+ type: object
57
+ properties:
58
+ success:
59
+ type: boolean
60
+ page_id:
61
+ type: string
62
+ web_url:
63
+ type: string
64
+ published:
65
+ type: boolean
66
+
67
+ error_handling:
68
+ retry: 1
69
+ backoff_type: exponential
70
+ initial_delay_ms: 1000
71
+
72
+ tags: [microsoft, graph, sharepoint, publish, write]
73
+
74
+ examples:
75
+ - name: Publish an HTML announcement page
76
+ params:
77
+ site_id: 'contoso.sharepoint.com,11111111-1111-1111-1111-111111111111'
78
+ title: 'Q3 results are in'
79
+ content: '<h2>We hit our targets</h2><p>Thanks to everyone who contributed.</p>'
80
+ - name: Create a plain-text draft without publishing
81
+ params:
82
+ site_id: 'contoso.sharepoint.com,11111111-1111-1111-1111-111111111111'
83
+ title: 'Draft: Office relocation FAQ'
84
+ content: 'This page is still being reviewed by Facilities.'
85
+ content_type: text
86
+ publish: false
87
+
88
+ notes:
89
+ caution: >-
90
+ Publishing makes the page visible to everyone with access to the site. Marked
91
+ risk: high and requires_approval: true so Matimo routes it through the
92
+ human-in-the-loop approval flow before it executes.
@@ -0,0 +1,126 @@
1
+ /**
2
+ * ms_publish_to_sharepoint
3
+ * Create: POST /sites/{site-id}/pages
4
+ * https://learn.microsoft.com/en-us/graph/api/sitepage-create
5
+ * Publish: POST /sites/{site-id}/pages/{page-id}/microsoft.graph.sitePage/publish
6
+ * https://learn.microsoft.com/en-us/graph/api/sitepage-publish
7
+ *
8
+ * Site pages always store web part bodies as HTML, so plain-text content is
9
+ * HTML-escaped and wrapped in a single <p> before being placed in a textWebPart.
10
+ */
11
+ import { MatimoError, ErrorCode } from '@matimo/core';
12
+ import { getAccessToken, requireParams, graphRequest, type ToolContext } from '../graph-client';
13
+
14
+ const VALID_CONTENT_TYPES = ['html', 'text'];
15
+
16
+ interface SitePage {
17
+ id?: string;
18
+ webUrl?: string;
19
+ }
20
+
21
+ const HTML_ESCAPES: Record<string, string> = {
22
+ '&': '&amp;',
23
+ '<': '&lt;',
24
+ '>': '&gt;',
25
+ '"': '&quot;',
26
+ "'": '&#39;',
27
+ };
28
+
29
+ function escapeHtml(text: string): string {
30
+ return text.replace(/[&<>"']/g, (ch) => HTML_ESCAPES[ch]);
31
+ }
32
+
33
+ function deriveFileName(title: string): string {
34
+ const slug = title
35
+ .toLowerCase()
36
+ .trim()
37
+ .replace(/[^a-z0-9]+/g, '-')
38
+ .replace(/^-+|-+$/g, '');
39
+ return `${slug || 'page'}.aspx`;
40
+ }
41
+
42
+ export default async function execute(
43
+ params: Record<string, unknown>,
44
+ context?: ToolContext
45
+ ): Promise<unknown> {
46
+ requireParams(params, ['site_id', 'title', 'content'], 'ms_publish_to_sharepoint');
47
+
48
+ const siteId = String(params.site_id);
49
+ const title = String(params.title);
50
+
51
+ const contentType = params.content_type === undefined ? 'html' : String(params.content_type);
52
+ if (!VALID_CONTENT_TYPES.includes(contentType)) {
53
+ throw new MatimoError(
54
+ `ms_publish_to_sharepoint: 'content_type' must be one of ${VALID_CONTENT_TYPES.join(', ')} (received '${contentType}')`,
55
+ ErrorCode.VALIDATION_FAILED,
56
+ { content_type: params.content_type }
57
+ );
58
+ }
59
+
60
+ const rawContent = String(params.content);
61
+ const innerHtml = contentType === 'text' ? `<p>${escapeHtml(rawContent)}</p>` : rawContent;
62
+
63
+ const shouldPublish = params.publish === undefined ? true : params.publish === true;
64
+
65
+ const token = getAccessToken(context);
66
+
67
+ const page = await graphRequest<SitePage>({
68
+ method: 'POST',
69
+ path: `/sites/${encodeURIComponent(siteId)}/pages`,
70
+ token,
71
+ resourceType: 'SharePoint site',
72
+ body: {
73
+ '@odata.type': '#microsoft.graph.sitePage',
74
+ name: deriveFileName(title),
75
+ title,
76
+ pageLayout: 'article',
77
+ canvasLayout: {
78
+ horizontalSections: [
79
+ {
80
+ layout: 'oneColumn',
81
+ id: '1',
82
+ emphasis: 'none',
83
+ columns: [
84
+ {
85
+ id: '1',
86
+ width: 12,
87
+ webparts: [
88
+ {
89
+ '@odata.type': '#microsoft.graph.textWebPart',
90
+ innerHtml,
91
+ },
92
+ ],
93
+ },
94
+ ],
95
+ },
96
+ ],
97
+ },
98
+ },
99
+ });
100
+
101
+ const pageId = page?.id;
102
+ if (!pageId) {
103
+ throw new MatimoError(
104
+ 'ms_publish_to_sharepoint: Microsoft Graph did not return an ID for the created page.',
105
+ ErrorCode.EXECUTION_FAILED,
106
+ { page }
107
+ );
108
+ }
109
+
110
+ if (shouldPublish) {
111
+ await graphRequest({
112
+ method: 'POST',
113
+ path: `/sites/${encodeURIComponent(siteId)}/pages/${encodeURIComponent(pageId)}/microsoft.graph.sitePage/publish`,
114
+ token,
115
+ resourceType: 'SharePoint page',
116
+ allowEmptyResponse: true,
117
+ });
118
+ }
119
+
120
+ return {
121
+ success: true,
122
+ page_id: pageId,
123
+ web_url: page?.webUrl ?? '',
124
+ published: shouldPublish,
125
+ };
126
+ }