@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.
- package/README.md +104 -0
- package/definition.yaml +61 -0
- package/package.json +18 -0
- package/tools/graph-client.ts +233 -0
- package/tools/ms_create_calendar_event/definition.yaml +100 -0
- package/tools/ms_create_calendar_event/ms_create_calendar_event.ts +79 -0
- package/tools/ms_create_document/definition.yaml +103 -0
- package/tools/ms_create_document/ms_create_document.ts +96 -0
- package/tools/ms_get_email/definition.yaml +88 -0
- package/tools/ms_get_email/ms_get_email.ts +94 -0
- package/tools/ms_list_files/definition.yaml +81 -0
- package/tools/ms_list_files/ms_list_files.ts +71 -0
- package/tools/ms_publish_to_sharepoint/definition.yaml +92 -0
- package/tools/ms_publish_to_sharepoint/ms_publish_to_sharepoint.ts +126 -0
- package/tools/ms_read_file/definition.yaml +74 -0
- package/tools/ms_read_file/ms_read_file.ts +102 -0
- package/tools/ms_search_knowledge/definition.yaml +99 -0
- package/tools/ms_search_knowledge/ms_search_knowledge.ts +109 -0
- package/tools/ms_send_email/definition.yaml +94 -0
- package/tools/ms_send_email/ms_send_email.ts +98 -0
- package/tools/ms_send_teams_message/definition.yaml +87 -0
- package/tools/ms_send_teams_message/ms_send_teams_message.ts +69 -0
|
@@ -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.
|