@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,74 @@
1
+ name: ms_read_file
2
+ description: |
3
+ Read the contents of a file stored in OneDrive or a SharePoint document library
4
+ (GET /drives/{drive_id}/items/{item_id}/content). Plain-text formats (text, HTML,
5
+ CSV, JSON, Markdown, XML) are decoded and returned as UTF-8 text. Rich document
6
+ formats (PDF, Word, Excel, PowerPoint) and other binary formats are NOT extracted —
7
+ the tool returns an empty `content` string plus a `warning` explaining why, so
8
+ agents can fall back to sharing the file's web URL instead of guessing at content.
9
+ version: '1.0.0'
10
+ status: approved
11
+ risk: low
12
+
13
+ parameters:
14
+ drive_id:
15
+ type: string
16
+ description: ID of the drive (OneDrive or SharePoint document library) containing the file
17
+ required: true
18
+
19
+ item_id:
20
+ type: string
21
+ description: ID of the drive item (file) to read
22
+ required: true
23
+
24
+ execution:
25
+ type: function
26
+ code: ms_read_file.ts
27
+ timeout: 30000
28
+
29
+ authentication:
30
+ type: oauth2
31
+ provider: microsoft
32
+ scopes:
33
+ - https://graph.microsoft.com/Files.Read.All
34
+
35
+ output_schema:
36
+ type: object
37
+ properties:
38
+ success:
39
+ type: boolean
40
+ content:
41
+ type: string
42
+ description: UTF-8 text content of the file, or an empty string if extraction is not supported
43
+ name:
44
+ type: string
45
+ mime_type:
46
+ type: string
47
+ size_bytes:
48
+ type: number
49
+ warning:
50
+ type: string
51
+ description: Present only when text extraction was not performed for this file's format
52
+
53
+ error_handling:
54
+ retry: 2
55
+ backoff_type: exponential
56
+ initial_delay_ms: 500
57
+
58
+ tags: [microsoft, graph, files, onedrive, sharepoint, read]
59
+
60
+ examples:
61
+ - name: Read a plain-text README from OneDrive
62
+ params:
63
+ drive_id: 'b!xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
64
+ item_id: '01ABCXYZ7654321'
65
+ - name: Read a CSV export from a SharePoint document library
66
+ params:
67
+ drive_id: 'b!yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy'
68
+ item_id: '01DEF9876543210'
69
+
70
+ notes:
71
+ caution: >-
72
+ Rich document formats (PDF, DOCX, XLSX, PPTX, legacy DOC/XLS) and other binary
73
+ formats return `content: ""` with a `warning` rather than extracted text — this
74
+ tool intentionally avoids bundling unverified third-party parsing libraries.
@@ -0,0 +1,102 @@
1
+ /**
2
+ * ms_read_file — GET /drives/{drive_id}/items/{item_id}/content
3
+ * https://learn.microsoft.com/en-us/graph/api/driveitem-get-content
4
+ *
5
+ * Scope decision (documented, not a shortcut): this tool performs REAL UTF-8 text
6
+ * extraction only for plain-text formats. Rich document formats (PDF/Word/Excel/
7
+ * PowerPoint) return `content: ""` with a format-specific warning rather than
8
+ * bundling unverified parsing dependencies (no matimo package currently depends on
9
+ * pdf-parse/mammoth/xlsx/cheerio). Truly-unsupported binaries get the exact warning
10
+ * the tool's contract specifies: "Binary file — text extraction not supported".
11
+ */
12
+ import { getAccessToken, requireParams, graphRequest, type ToolContext } from '../graph-client';
13
+
14
+ const TEXT_MIME_PREFIXES = ['text/'];
15
+ const TEXT_MIME_TYPES = new Set(['application/json', 'application/xml']);
16
+
17
+ const RICH_DOCUMENT_LABELS: Record<string, string> = {
18
+ 'application/pdf': 'PDF document',
19
+ 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': 'Word document (.docx)',
20
+ 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': 'Excel workbook (.xlsx)',
21
+ 'application/vnd.openxmlformats-officedocument.presentationml.presentation':
22
+ 'PowerPoint presentation (.pptx)',
23
+ 'application/msword': 'Word document (.doc)',
24
+ 'application/vnd.ms-excel': 'Excel workbook (.xls)',
25
+ 'application/vnd.ms-powerpoint': 'PowerPoint presentation (.ppt)',
26
+ };
27
+
28
+ interface DriveItemMetadata {
29
+ name?: string;
30
+ size?: number;
31
+ file?: { mimeType?: string };
32
+ }
33
+
34
+ function isPlainTextMime(mimeType: string): boolean {
35
+ return TEXT_MIME_TYPES.has(mimeType) || TEXT_MIME_PREFIXES.some((p) => mimeType.startsWith(p));
36
+ }
37
+
38
+ export default async function execute(
39
+ params: Record<string, unknown>,
40
+ context?: ToolContext
41
+ ): Promise<unknown> {
42
+ requireParams(params, ['drive_id', 'item_id'], 'ms_read_file');
43
+
44
+ const driveId = String(params.drive_id);
45
+ const itemId = String(params.item_id);
46
+ const token = getAccessToken(context);
47
+
48
+ const metadata = await graphRequest<DriveItemMetadata>({
49
+ method: 'GET',
50
+ path: `/drives/${encodeURIComponent(driveId)}/items/${encodeURIComponent(itemId)}`,
51
+ token,
52
+ resourceType: 'Drive item',
53
+ query: { $select: 'name,size,file' },
54
+ });
55
+
56
+ const name = metadata?.name ?? '';
57
+ const mimeType = metadata?.file?.mimeType ?? 'application/octet-stream';
58
+ const sizeBytes = metadata?.size ?? 0;
59
+
60
+ const raw = await graphRequest<ArrayBuffer>({
61
+ method: 'GET',
62
+ path: `/drives/${encodeURIComponent(driveId)}/items/${encodeURIComponent(itemId)}/content`,
63
+ token,
64
+ resourceType: 'Drive item content',
65
+ responseType: 'arraybuffer',
66
+ });
67
+
68
+ const buffer = Buffer.from(raw);
69
+
70
+ if (isPlainTextMime(mimeType)) {
71
+ return {
72
+ success: true,
73
+ content: buffer.toString('utf-8'),
74
+ name,
75
+ mime_type: mimeType,
76
+ size_bytes: sizeBytes,
77
+ };
78
+ }
79
+
80
+ const richDocumentLabel = RICH_DOCUMENT_LABELS[mimeType];
81
+ if (richDocumentLabel) {
82
+ return {
83
+ success: true,
84
+ content: '',
85
+ name,
86
+ mime_type: mimeType,
87
+ size_bytes: sizeBytes,
88
+ warning:
89
+ `${richDocumentLabel} — text extraction for this format is not implemented to avoid ` +
90
+ 'bundling unverified parsing dependencies. Share the file via its web URL instead.',
91
+ };
92
+ }
93
+
94
+ return {
95
+ success: true,
96
+ content: '',
97
+ name,
98
+ mime_type: mimeType,
99
+ size_bytes: sizeBytes,
100
+ warning: 'Binary file — text extraction not supported',
101
+ };
102
+ }
@@ -0,0 +1,99 @@
1
+ name: ms_search_knowledge
2
+ description: |
3
+ Search across Microsoft 365 content — SharePoint sites, OneDrive/SharePoint document
4
+ libraries, and list items — using the Microsoft Search API (POST /search/query).
5
+ Returns ranked hits with summaries and links, useful for grounding agent answers in
6
+ organizational knowledge.
7
+ version: '1.0.0'
8
+ status: approved
9
+ risk: low
10
+
11
+ parameters:
12
+ query:
13
+ type: string
14
+ description: 'Search query string. Supports KQL syntax, e.g. "quarterly report filetype:xlsx"'
15
+ required: true
16
+
17
+ entity_types:
18
+ type: array
19
+ description: Microsoft Search entity types to search across
20
+ required: false
21
+ default: [driveItem, listItem, site]
22
+
23
+ site_id:
24
+ type: string
25
+ description: >-
26
+ Optional SharePoint site ID used to scope the search. The Microsoft Search API has
27
+ no dedicated site filter for these entity types, so this is folded into the query
28
+ string as a best-effort hint rather than a guaranteed server-side filter.
29
+ required: false
30
+
31
+ drive_id:
32
+ type: string
33
+ description: >-
34
+ Optional OneDrive/SharePoint drive ID used to scope the search. Same best-effort
35
+ caveat as site_id — there is no dedicated drive filter in the Search API for these
36
+ entity types.
37
+ required: false
38
+
39
+ top:
40
+ type: number
41
+ description: Maximum number of results to return (1-25)
42
+ required: false
43
+ default: 10
44
+
45
+ execution:
46
+ type: function
47
+ code: ms_search_knowledge.ts
48
+ timeout: 20000
49
+
50
+ authentication:
51
+ type: oauth2
52
+ provider: microsoft
53
+ scopes:
54
+ - https://graph.microsoft.com/Sites.Read.All
55
+ - https://graph.microsoft.com/Files.Read.All
56
+
57
+ output_schema:
58
+ type: object
59
+ properties:
60
+ success:
61
+ type: boolean
62
+ results:
63
+ type: array
64
+ items:
65
+ type: object
66
+ properties:
67
+ id:
68
+ type: string
69
+ name:
70
+ type: string
71
+ summary:
72
+ type: string
73
+ web_url:
74
+ type: string
75
+ last_modified:
76
+ type: string
77
+ score:
78
+ type: number
79
+ total_count:
80
+ type: number
81
+ description: Total number of hits reported by Microsoft Search for this query
82
+
83
+ error_handling:
84
+ retry: 2
85
+ backoff_type: exponential
86
+ initial_delay_ms: 500
87
+
88
+ tags: [microsoft, graph, search, knowledge, sharepoint, onedrive]
89
+
90
+ examples:
91
+ - name: Search the tenant for a budget spreadsheet
92
+ params:
93
+ query: 'Q3 budget filetype:xlsx'
94
+ top: 5
95
+ - name: Search a specific SharePoint site for onboarding docs
96
+ params:
97
+ query: 'onboarding checklist'
98
+ site_id: 'contoso.sharepoint.com,11111111-1111-1111-1111-111111111111'
99
+ entity_types: [driveItem, listItem]
@@ -0,0 +1,109 @@
1
+ /**
2
+ * ms_search_knowledge — POST /search/query
3
+ * https://learn.microsoft.com/en-us/graph/api/search-query
4
+ */
5
+ import { MatimoError, ErrorCode } from '@matimo/core';
6
+ import { getAccessToken, requireParams, graphRequest, type ToolContext } from '../graph-client';
7
+
8
+ const VALID_ENTITY_TYPES = ['driveItem', 'listItem', 'site', 'list', 'drive'];
9
+ const DEFAULT_ENTITY_TYPES = ['driveItem', 'listItem', 'site'];
10
+ const DEFAULT_TOP = 10;
11
+ const MAX_TOP = 25;
12
+
13
+ interface SearchHit {
14
+ hitId?: string;
15
+ rank?: number;
16
+ summary?: string;
17
+ resource?: {
18
+ id?: string;
19
+ name?: string;
20
+ webUrl?: string;
21
+ lastModifiedDateTime?: string;
22
+ };
23
+ }
24
+
25
+ interface SearchResponse {
26
+ value?: Array<{
27
+ hitsContainers?: Array<{
28
+ total?: number;
29
+ hits?: SearchHit[];
30
+ }>;
31
+ }>;
32
+ }
33
+
34
+ export default async function execute(
35
+ params: Record<string, unknown>,
36
+ context?: ToolContext
37
+ ): Promise<unknown> {
38
+ requireParams(params, ['query'], 'ms_search_knowledge');
39
+
40
+ const query = String(params.query);
41
+
42
+ const entityTypes = Array.isArray(params.entity_types)
43
+ ? (params.entity_types as unknown[]).map(String)
44
+ : DEFAULT_ENTITY_TYPES;
45
+
46
+ const invalidEntityTypes = entityTypes.filter((t) => !VALID_ENTITY_TYPES.includes(t));
47
+ if (entityTypes.length === 0 || invalidEntityTypes.length > 0) {
48
+ throw new MatimoError(
49
+ `ms_search_knowledge: invalid entity_types ${JSON.stringify(invalidEntityTypes)}. ` +
50
+ `Valid values are: ${VALID_ENTITY_TYPES.join(', ')}`,
51
+ ErrorCode.VALIDATION_FAILED,
52
+ { entityTypes, invalidEntityTypes }
53
+ );
54
+ }
55
+
56
+ const top = params.top === undefined ? DEFAULT_TOP : Number(params.top);
57
+ if (!Number.isFinite(top) || top < 1 || top > MAX_TOP) {
58
+ throw new MatimoError(
59
+ `ms_search_knowledge: 'top' must be a number between 1 and ${MAX_TOP} (received ${String(params.top)})`,
60
+ ErrorCode.VALIDATION_FAILED,
61
+ { top: params.top }
62
+ );
63
+ }
64
+
65
+ // Microsoft Search has no dedicated site/drive filter for driveItem/listItem/site
66
+ // entity types — fold the IDs into the query string as a best-effort scoping hint.
67
+ // This is documented in the tool description so callers don't expect a hard filter.
68
+ const scopeHints = [params.site_id, params.drive_id].filter(
69
+ (value): value is string => typeof value === 'string' && value.length > 0
70
+ );
71
+ const queryString = scopeHints.length > 0 ? `${query} ${scopeHints.join(' ')}` : query;
72
+
73
+ const token = getAccessToken(context);
74
+
75
+ const data = await graphRequest<SearchResponse>({
76
+ method: 'POST',
77
+ path: '/search/query',
78
+ token,
79
+ resourceType: 'Search results',
80
+ body: {
81
+ requests: [
82
+ {
83
+ entityTypes,
84
+ query: { queryString },
85
+ from: 0,
86
+ size: top,
87
+ },
88
+ ],
89
+ },
90
+ });
91
+
92
+ const container = data?.value?.[0]?.hitsContainers?.[0];
93
+ const hits = container?.hits ?? [];
94
+
95
+ const results = hits.map((hit) => ({
96
+ id: hit.resource?.id ?? hit.hitId ?? '',
97
+ name: hit.resource?.name ?? '',
98
+ summary: hit.summary ?? '',
99
+ web_url: hit.resource?.webUrl ?? '',
100
+ last_modified: hit.resource?.lastModifiedDateTime ?? '',
101
+ score: hit.rank ?? 0,
102
+ }));
103
+
104
+ return {
105
+ success: true,
106
+ results,
107
+ total_count: container?.total ?? results.length,
108
+ };
109
+ }
@@ -0,0 +1,94 @@
1
+ name: ms_send_email
2
+ description: |
3
+ Send an email as the signed-in user. Microsoft Graph's /me/sendMail endpoint returns
4
+ an empty 202 Accepted with no message identifier, so this tool creates a draft first
5
+ (POST /me/messages, which returns an `id`) and then sends that draft
6
+ (POST /me/messages/{id}/send) — giving callers a real `message_id` to reference
7
+ afterwards. High-risk: this sends mail on the user's behalf and requires approval.
8
+ version: '1.0.0'
9
+ status: approved
10
+ risk: high
11
+ requires_approval: true
12
+
13
+ parameters:
14
+ to:
15
+ type: array
16
+ description: Recipient email addresses
17
+ required: true
18
+
19
+ subject:
20
+ type: string
21
+ description: Subject line of the email
22
+ required: true
23
+
24
+ body:
25
+ type: string
26
+ description: Body content of the email
27
+ required: true
28
+
29
+ body_type:
30
+ type: string
31
+ description: Whether `body` is plain text or HTML
32
+ required: false
33
+ default: text
34
+ enum: [text, html]
35
+
36
+ cc:
37
+ type: array
38
+ description: CC recipient email addresses
39
+ required: false
40
+
41
+ bcc:
42
+ type: array
43
+ description: BCC recipient email addresses
44
+ required: false
45
+
46
+ execution:
47
+ type: function
48
+ code: ms_send_email.ts
49
+ timeout: 30000
50
+
51
+ authentication:
52
+ type: oauth2
53
+ provider: microsoft
54
+ scopes:
55
+ - https://graph.microsoft.com/Mail.Send
56
+ - https://graph.microsoft.com/Mail.ReadWrite
57
+
58
+ output_schema:
59
+ type: object
60
+ properties:
61
+ success:
62
+ type: boolean
63
+ sent:
64
+ type: boolean
65
+ message_id:
66
+ type: string
67
+ description: ID of the sent message (obtained from the draft creation step)
68
+
69
+ error_handling:
70
+ retry: 1
71
+ backoff_type: exponential
72
+ initial_delay_ms: 1000
73
+
74
+ tags: [microsoft, graph, mail, outlook, send, write]
75
+
76
+ examples:
77
+ - name: Send a plain-text email to one recipient
78
+ params:
79
+ to: ['alice@contoso.com']
80
+ subject: 'Weekly status update'
81
+ body: 'Here is the summary for this week...'
82
+ - name: Send an HTML email with CC
83
+ params:
84
+ to: ['team@contoso.com']
85
+ cc: ['manager@contoso.com']
86
+ subject: 'Release notes — v2.4.0'
87
+ body: '<h1>v2.4.0</h1><p>Highlights...</p>'
88
+ body_type: html
89
+
90
+ notes:
91
+ caution: >-
92
+ This tool sends real email on behalf of the connected user. It is marked
93
+ risk: high and requires_approval: true — Matimo will route it through the
94
+ human-in-the-loop approval flow before it executes.
@@ -0,0 +1,98 @@
1
+ /**
2
+ * ms_send_email — draft + send, two Graph calls
3
+ * 1. POST /me/messages https://learn.microsoft.com/en-us/graph/api/user-post-messages
4
+ * 2. POST /me/messages/{id}/send https://learn.microsoft.com/en-us/graph/api/message-send
5
+ *
6
+ * Why two calls: POST /me/sendMail returns an empty 202 Accepted with no message
7
+ * identifier, but this tool's contract promises a `message_id`. Creating a draft
8
+ * first gives us a real message ID we can report back, then we send that draft.
9
+ */
10
+ import { MatimoError, ErrorCode } from '@matimo/core';
11
+ import { getAccessToken, requireParams, graphRequest, type ToolContext } from '../graph-client';
12
+
13
+ const VALID_BODY_TYPES = ['text', 'html'];
14
+
15
+ interface DraftMessage {
16
+ id?: string;
17
+ }
18
+
19
+ function toRecipientList(value: unknown, fieldName: string): Array<{ emailAddress: { address: string } }> {
20
+ if (value === undefined) return [];
21
+ if (!Array.isArray(value) || value.some((entry) => typeof entry !== 'string' || !entry)) {
22
+ throw new MatimoError(
23
+ `ms_send_email: '${fieldName}' must be an array of email address strings`,
24
+ ErrorCode.VALIDATION_FAILED,
25
+ { field: fieldName, received: value }
26
+ );
27
+ }
28
+ return (value as string[]).map((address) => ({ emailAddress: { address } }));
29
+ }
30
+
31
+ export default async function execute(
32
+ params: Record<string, unknown>,
33
+ context?: ToolContext
34
+ ): Promise<unknown> {
35
+ requireParams(params, ['to', 'subject', 'body'], 'ms_send_email');
36
+
37
+ const to = toRecipientList(params.to, 'to');
38
+ if (to.length === 0) {
39
+ throw new MatimoError(
40
+ "ms_send_email: 'to' must contain at least one recipient email address",
41
+ ErrorCode.VALIDATION_FAILED,
42
+ { to: params.to }
43
+ );
44
+ }
45
+ const cc = toRecipientList(params.cc, 'cc');
46
+ const bcc = toRecipientList(params.bcc, 'bcc');
47
+
48
+ const bodyType = params.body_type === undefined ? 'text' : String(params.body_type);
49
+ if (!VALID_BODY_TYPES.includes(bodyType)) {
50
+ throw new MatimoError(
51
+ `ms_send_email: 'body_type' must be one of ${VALID_BODY_TYPES.join(', ')} (received '${bodyType}')`,
52
+ ErrorCode.VALIDATION_FAILED,
53
+ { body_type: params.body_type }
54
+ );
55
+ }
56
+
57
+ const token = getAccessToken(context);
58
+
59
+ const draft = await graphRequest<DraftMessage>({
60
+ method: 'POST',
61
+ path: '/me/messages',
62
+ token,
63
+ resourceType: 'Mail draft',
64
+ body: {
65
+ subject: String(params.subject),
66
+ body: {
67
+ contentType: bodyType === 'html' ? 'HTML' : 'Text',
68
+ content: String(params.body),
69
+ },
70
+ toRecipients: to,
71
+ ...(cc.length > 0 ? { ccRecipients: cc } : {}),
72
+ ...(bcc.length > 0 ? { bccRecipients: bcc } : {}),
73
+ },
74
+ });
75
+
76
+ const messageId = draft?.id;
77
+ if (!messageId) {
78
+ throw new MatimoError(
79
+ 'ms_send_email: Microsoft Graph did not return an ID for the created draft message.',
80
+ ErrorCode.EXECUTION_FAILED,
81
+ { draft }
82
+ );
83
+ }
84
+
85
+ await graphRequest({
86
+ method: 'POST',
87
+ path: `/me/messages/${encodeURIComponent(messageId)}/send`,
88
+ token,
89
+ resourceType: 'Mail draft',
90
+ allowEmptyResponse: true,
91
+ });
92
+
93
+ return {
94
+ success: true,
95
+ sent: true,
96
+ message_id: messageId,
97
+ };
98
+ }
@@ -0,0 +1,87 @@
1
+ name: ms_send_teams_message
2
+ description: |
3
+ Post a message to a Microsoft Teams channel (POST /teams/{team_id}/channels/{channel_id}/messages),
4
+ optionally as a reply to an existing message
5
+ (POST /teams/{team_id}/channels/{channel_id}/messages/{reply_to_message_id}/replies).
6
+ version: '1.0.0'
7
+ status: approved
8
+ risk: medium
9
+
10
+ parameters:
11
+ team_id:
12
+ type: string
13
+ description: ID of the team that owns the channel
14
+ required: true
15
+
16
+ channel_id:
17
+ type: string
18
+ description: ID of the channel to post the message to
19
+ required: true
20
+
21
+ text:
22
+ type: string
23
+ description: Message content
24
+ required: true
25
+
26
+ reply_to_message_id:
27
+ type: string
28
+ description: >-
29
+ ID of an existing message to reply to. When provided, the message is posted
30
+ as a threaded reply instead of a new top-level channel message.
31
+ required: false
32
+
33
+ content_type:
34
+ type: string
35
+ description: Whether `text` is plain text or HTML
36
+ required: false
37
+ default: text
38
+ enum: [text, html]
39
+
40
+ execution:
41
+ type: function
42
+ code: ms_send_teams_message.ts
43
+ timeout: 20000
44
+
45
+ authentication:
46
+ type: oauth2
47
+ provider: microsoft
48
+ scopes:
49
+ - https://graph.microsoft.com/ChannelMessage.Send
50
+
51
+ output_schema:
52
+ type: object
53
+ properties:
54
+ success:
55
+ type: boolean
56
+ message_id:
57
+ type: string
58
+ web_url:
59
+ type: string
60
+ created_at:
61
+ type: string
62
+
63
+ error_handling:
64
+ retry: 1
65
+ backoff_type: exponential
66
+ initial_delay_ms: 1000
67
+
68
+ tags: [microsoft, graph, teams, chat, write]
69
+
70
+ examples:
71
+ - name: Post a status update to a channel
72
+ params:
73
+ team_id: '11111111-1111-1111-1111-111111111111'
74
+ channel_id: '19:abcdefgh@thread.tacv2'
75
+ text: 'Deployment to production completed successfully ✅'
76
+ - name: Reply to an existing thread
77
+ params:
78
+ team_id: '11111111-1111-1111-1111-111111111111'
79
+ channel_id: '19:abcdefgh@thread.tacv2'
80
+ reply_to_message_id: '1700000000000'
81
+ text: 'Following up — this is now resolved.'
82
+
83
+ notes:
84
+ caution: >-
85
+ Posts a real message visible to the channel's members. Marked risk: medium —
86
+ review the policy tier configuration if your deployment requires HITL approval
87
+ for Teams writes as well.