@matimo/microsoft 0.1.0 → 0.1.4
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/package.json +8 -5
- package/tools/graph-client.js +132 -0
- package/tools/graph-client.ts +1 -1
- package/tools/ms_create_calendar_event/definition.yaml +1 -1
- package/tools/ms_create_calendar_event/ms_create_calendar_event.js +54 -0
- package/tools/ms_create_calendar_event/ms_create_calendar_event.ts +6 -2
- package/tools/ms_create_document/definition.yaml +1 -1
- package/tools/ms_create_document/ms_create_document.js +59 -0
- package/tools/ms_create_document/ms_create_document.ts +6 -2
- package/tools/ms_get_email/definition.yaml +1 -1
- package/tools/ms_get_email/ms_get_email.js +56 -0
- package/tools/ms_get_email/ms_get_email.ts +6 -2
- package/tools/ms_list_files/definition.yaml +1 -1
- package/tools/ms_list_files/ms_list_files.js +42 -0
- package/tools/ms_list_files/ms_list_files.ts +6 -2
- package/tools/ms_publish_to_sharepoint/definition.yaml +1 -1
- package/tools/ms_publish_to_sharepoint/ms_publish_to_sharepoint.js +96 -0
- package/tools/ms_publish_to_sharepoint/ms_publish_to_sharepoint.ts +6 -2
- package/tools/ms_read_file/definition.yaml +1 -1
- package/tools/ms_read_file/ms_read_file.js +79 -0
- package/tools/ms_read_file/ms_read_file.ts +5 -1
- package/tools/ms_search_knowledge/definition.yaml +1 -1
- package/tools/ms_search_knowledge/ms_search_knowledge.js +63 -0
- package/tools/ms_search_knowledge/ms_search_knowledge.ts +6 -2
- package/tools/ms_send_email/definition.yaml +1 -1
- package/tools/ms_send_email/ms_send_email.js +66 -0
- package/tools/ms_send_email/ms_send_email.ts +6 -2
- package/tools/ms_send_teams_message/definition.yaml +1 -1
- package/tools/ms_send_teams_message/ms_send_teams_message.js +46 -0
- package/tools/ms_send_teams_message/ms_send_teams_message.ts +6 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@matimo/microsoft",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "Microsoft Graph tools for Matimo (search, files, mail, Teams, calendar, SharePoint)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -8,11 +8,14 @@
|
|
|
8
8
|
"README.md",
|
|
9
9
|
"definition.yaml"
|
|
10
10
|
],
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"axios": "^1.15.2",
|
|
13
|
+
"@matimo/core": "0.1.4"
|
|
14
|
+
},
|
|
11
15
|
"peerDependencies": {
|
|
12
|
-
"matimo": "0.1.
|
|
16
|
+
"matimo": "0.1.4"
|
|
13
17
|
},
|
|
14
|
-
"
|
|
15
|
-
"
|
|
16
|
-
"@matimo/core": "0.1.3"
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "tsc"
|
|
17
20
|
}
|
|
18
21
|
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared Microsoft Graph helpers for all `type: function` tools in this package.
|
|
3
|
+
*
|
|
4
|
+
* Conventions (mirrors @matimo/slack and @matimo/gmail):
|
|
5
|
+
* - Tools NEVER perform OAuth token exchange. A delegated Graph access token is
|
|
6
|
+
* injected at execution time via `context.credentials.MICROSOFT_GRAPH_ACCESS_TOKEN`
|
|
7
|
+
* (or the MICROSOFT_GRAPH_ACCESS_TOKEN environment variable as a fallback).
|
|
8
|
+
* - Every Graph error is normalized into a MatimoError with the closest matching
|
|
9
|
+
* ErrorCode (Matimo has no per-provider error classes — see errors/matimo-error.ts).
|
|
10
|
+
*/
|
|
11
|
+
import axios from 'axios';
|
|
12
|
+
import { MatimoError, ErrorCode } from '@matimo/core/runtime';
|
|
13
|
+
export const GRAPH_BASE_URL = 'https://graph.microsoft.com/v1.0';
|
|
14
|
+
const RETRYABLE_STATUS_CODES = new Set([429, 500, 503]);
|
|
15
|
+
const MAX_RETRIES = 3;
|
|
16
|
+
const INITIAL_BACKOFF_MS = 500;
|
|
17
|
+
/**
|
|
18
|
+
* Resolve the delegated Graph access token. Matimo never exchanges OAuth codes —
|
|
19
|
+
* the token must already be present in per-call credentials or the environment.
|
|
20
|
+
*/
|
|
21
|
+
export function getAccessToken(context) {
|
|
22
|
+
const token = context?.credentials?.MICROSOFT_GRAPH_ACCESS_TOKEN ?? process.env.MICROSOFT_GRAPH_ACCESS_TOKEN;
|
|
23
|
+
if (!token) {
|
|
24
|
+
throw new MatimoError('Microsoft Graph access token is missing. Provide it via credentials.MICROSOFT_GRAPH_ACCESS_TOKEN ' +
|
|
25
|
+
'or the MICROSOFT_GRAPH_ACCESS_TOKEN environment variable. Matimo never performs the OAuth ' +
|
|
26
|
+
'exchange itself — connect Microsoft in Nova first.', ErrorCode.AUTH_FAILED, { provider: 'microsoft', placeholder: 'MICROSOFT_GRAPH_ACCESS_TOKEN' });
|
|
27
|
+
}
|
|
28
|
+
return token;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Validate required parameters BEFORE any network call, mirroring the
|
|
32
|
+
* "ValidationError before any API call" requirement. Throws VALIDATION_FAILED.
|
|
33
|
+
*/
|
|
34
|
+
export function requireParams(params, required, toolName) {
|
|
35
|
+
const missing = required.filter((name) => {
|
|
36
|
+
const value = params[name];
|
|
37
|
+
return value === undefined || value === null || value === '';
|
|
38
|
+
});
|
|
39
|
+
if (missing.length > 0) {
|
|
40
|
+
throw new MatimoError(`${toolName}: missing required parameter(s): ${missing.join(', ')}`, ErrorCode.VALIDATION_FAILED, { toolName, missingParams: missing });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Map a Microsoft Graph HTTP error response onto a MatimoError using the closest
|
|
45
|
+
* matching ErrorCode (Matimo has no CredentialError/NotFoundError/ProviderError
|
|
46
|
+
* classes — see typescript/packages/core/src/errors/matimo-error.ts):
|
|
47
|
+
* 401/403 -> AUTH_FAILED ("Microsoft Graph access denied. Check connection status in Nova.")
|
|
48
|
+
* 404 -> FILE_NOT_FOUND (details.resourceType identifies what was missing)
|
|
49
|
+
* 429 -> RATE_LIMIT_EXCEEDED (details.retryAfterSeconds carries Retry-After)
|
|
50
|
+
* 500/503 -> EXECUTION_FAILED (retryable)
|
|
51
|
+
* other -> EXECUTION_FAILED
|
|
52
|
+
*/
|
|
53
|
+
export function mapGraphError(status, data, headers, resourceType) {
|
|
54
|
+
const graphError = data?.error;
|
|
55
|
+
const details = { statusCode: status, graphError, resourceType };
|
|
56
|
+
if (status === 401 || status === 403) {
|
|
57
|
+
return new MatimoError('Microsoft Graph access denied. Check connection status in Nova.', ErrorCode.AUTH_FAILED, details);
|
|
58
|
+
}
|
|
59
|
+
if (status === 404) {
|
|
60
|
+
return new MatimoError(`${resourceType} not found.`, ErrorCode.FILE_NOT_FOUND, details);
|
|
61
|
+
}
|
|
62
|
+
if (status === 429) {
|
|
63
|
+
const retryAfterHeader = headers?.['retry-after'] ?? headers?.['Retry-After'];
|
|
64
|
+
const retryAfterSeconds = retryAfterHeader !== undefined ? Number(retryAfterHeader) : undefined;
|
|
65
|
+
return new MatimoError('Microsoft Graph rate limit exceeded. Respect Retry-After before retrying.', ErrorCode.RATE_LIMIT_EXCEEDED, { ...details, retryAfterSeconds });
|
|
66
|
+
}
|
|
67
|
+
if (status === 500 || status === 503) {
|
|
68
|
+
return new MatimoError('Microsoft Graph service is temporarily unavailable. Please retry shortly.', ErrorCode.EXECUTION_FAILED, details);
|
|
69
|
+
}
|
|
70
|
+
return new MatimoError(`Microsoft Graph request failed with status ${status}.`, ErrorCode.EXECUTION_FAILED, details);
|
|
71
|
+
}
|
|
72
|
+
function buildQueryString(query) {
|
|
73
|
+
if (!query)
|
|
74
|
+
return '';
|
|
75
|
+
const parts = Object.entries(query)
|
|
76
|
+
.filter(([, value]) => value !== undefined && value !== null && value !== '')
|
|
77
|
+
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`);
|
|
78
|
+
return parts.length > 0 ? `?${parts.join('&')}` : '';
|
|
79
|
+
}
|
|
80
|
+
function sleep(ms) {
|
|
81
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Perform an authenticated Microsoft Graph request with retry-on-429/5xx
|
|
85
|
+
* (respecting Retry-After, exponential backoff, max 3 retries) and normalized
|
|
86
|
+
* MatimoError mapping for every other failure.
|
|
87
|
+
*/
|
|
88
|
+
export async function graphRequest(options) {
|
|
89
|
+
const { method, path, token, query, body, headers, resourceType = 'Resource', responseType = 'json', allowEmptyResponse = false, } = options;
|
|
90
|
+
const url = `${GRAPH_BASE_URL}${path}${buildQueryString(query)}`;
|
|
91
|
+
let attempt = 0;
|
|
92
|
+
for (;;) {
|
|
93
|
+
let response;
|
|
94
|
+
try {
|
|
95
|
+
response = await axios.request({
|
|
96
|
+
method,
|
|
97
|
+
url,
|
|
98
|
+
data: body,
|
|
99
|
+
responseType,
|
|
100
|
+
validateStatus: () => true,
|
|
101
|
+
headers: {
|
|
102
|
+
Authorization: `Bearer ${token}`,
|
|
103
|
+
...(responseType === 'json' ? { Accept: 'application/json' } : {}),
|
|
104
|
+
...(body !== undefined && !(body instanceof Buffer)
|
|
105
|
+
? { 'Content-Type': 'application/json' }
|
|
106
|
+
: {}),
|
|
107
|
+
...headers,
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
throw new MatimoError('Microsoft Graph request failed before a response was received (network error).', ErrorCode.NETWORK_ERROR, { path, originalError: error instanceof Error ? error.message : String(error) }, error);
|
|
113
|
+
}
|
|
114
|
+
if (response.status >= 200 && response.status < 300) {
|
|
115
|
+
if (allowEmptyResponse && (response.status === 204 || !response.data)) {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
return response.data;
|
|
119
|
+
}
|
|
120
|
+
const error = mapGraphError(response.status, response.data, response.headers, resourceType);
|
|
121
|
+
const isRetryable = RETRYABLE_STATUS_CODES.has(response.status) && attempt < MAX_RETRIES;
|
|
122
|
+
if (!isRetryable) {
|
|
123
|
+
throw error;
|
|
124
|
+
}
|
|
125
|
+
const retryAfterSeconds = error.details?.retryAfterSeconds;
|
|
126
|
+
const delayMs = retryAfterSeconds !== undefined && !Number.isNaN(retryAfterSeconds)
|
|
127
|
+
? retryAfterSeconds * 1000
|
|
128
|
+
: INITIAL_BACKOFF_MS * 2 ** attempt;
|
|
129
|
+
attempt += 1;
|
|
130
|
+
await sleep(delayMs);
|
|
131
|
+
}
|
|
132
|
+
}
|
package/tools/graph-client.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* ErrorCode (Matimo has no per-provider error classes — see errors/matimo-error.ts).
|
|
10
10
|
*/
|
|
11
11
|
import axios from 'axios';
|
|
12
|
-
import { MatimoError, ErrorCode } from '@matimo/core';
|
|
12
|
+
import { MatimoError, ErrorCode } from '@matimo/core/runtime';
|
|
13
13
|
|
|
14
14
|
export const GRAPH_BASE_URL = 'https://graph.microsoft.com/v1.0';
|
|
15
15
|
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ms_create_calendar_event — POST /me/events
|
|
3
|
+
* https://learn.microsoft.com/en-us/graph/api/user-post-events
|
|
4
|
+
*/
|
|
5
|
+
import { MatimoError, ErrorCode } from '@matimo/core/runtime';
|
|
6
|
+
import { getAccessToken, requireParams, graphRequest } from '../graph-client.js';
|
|
7
|
+
const DEFAULT_TIMEZONE = 'UTC';
|
|
8
|
+
function toAttendeeList(value) {
|
|
9
|
+
if (value === undefined)
|
|
10
|
+
return [];
|
|
11
|
+
if (!Array.isArray(value) || value.some((entry) => typeof entry !== 'string' || !entry)) {
|
|
12
|
+
throw new MatimoError("ms_create_calendar_event: 'attendees' must be an array of email address strings", ErrorCode.VALIDATION_FAILED, { attendees: value });
|
|
13
|
+
}
|
|
14
|
+
return value.map((address) => ({
|
|
15
|
+
emailAddress: { address },
|
|
16
|
+
type: 'required',
|
|
17
|
+
}));
|
|
18
|
+
}
|
|
19
|
+
export default async function execute(params, context) {
|
|
20
|
+
requireParams(params, ['subject', 'start', 'end'], 'ms_create_calendar_event');
|
|
21
|
+
const subject = String(params.subject);
|
|
22
|
+
const start = String(params.start);
|
|
23
|
+
const end = String(params.end);
|
|
24
|
+
const timezone = typeof params.timezone === 'string' && params.timezone ? params.timezone : DEFAULT_TIMEZONE;
|
|
25
|
+
const attendees = toAttendeeList(params.attendees);
|
|
26
|
+
const isOnlineMeeting = params.is_online_meeting === true;
|
|
27
|
+
const token = getAccessToken(context);
|
|
28
|
+
const event = await graphRequest({
|
|
29
|
+
method: 'POST',
|
|
30
|
+
path: '/me/events',
|
|
31
|
+
token,
|
|
32
|
+
resourceType: 'Calendar',
|
|
33
|
+
body: {
|
|
34
|
+
subject,
|
|
35
|
+
...(typeof params.body === 'string' && params.body
|
|
36
|
+
? { body: { contentType: 'Text', content: params.body } }
|
|
37
|
+
: {}),
|
|
38
|
+
start: { dateTime: start, timeZone: timezone },
|
|
39
|
+
end: { dateTime: end, timeZone: timezone },
|
|
40
|
+
...(attendees.length > 0 ? { attendees } : {}),
|
|
41
|
+
...(typeof params.location === 'string' && params.location
|
|
42
|
+
? { location: { displayName: params.location } }
|
|
43
|
+
: {}),
|
|
44
|
+
isOnlineMeeting,
|
|
45
|
+
...(isOnlineMeeting ? { onlineMeetingProvider: 'teamsForBusiness' } : {}),
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
return {
|
|
49
|
+
success: true,
|
|
50
|
+
event_id: event?.id ?? '',
|
|
51
|
+
web_link: event?.webLink ?? '',
|
|
52
|
+
...(event?.onlineMeeting?.joinUrl ? { join_url: event.onlineMeeting.joinUrl } : {}),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
@@ -2,8 +2,12 @@
|
|
|
2
2
|
* ms_create_calendar_event — POST /me/events
|
|
3
3
|
* https://learn.microsoft.com/en-us/graph/api/user-post-events
|
|
4
4
|
*/
|
|
5
|
-
import { MatimoError, ErrorCode } from '@matimo/core';
|
|
6
|
-
import { getAccessToken, requireParams, graphRequest
|
|
5
|
+
import { MatimoError, ErrorCode } from '@matimo/core/runtime';
|
|
6
|
+
import { getAccessToken, requireParams, graphRequest } from '../graph-client.js';
|
|
7
|
+
|
|
8
|
+
interface ToolContext {
|
|
9
|
+
credentials?: Record<string, string>;
|
|
10
|
+
}
|
|
7
11
|
|
|
8
12
|
const DEFAULT_TIMEZONE = 'UTC';
|
|
9
13
|
|
|
@@ -0,0 +1,59 @@
|
|
|
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/runtime';
|
|
10
|
+
import { getAccessToken, requireParams, graphRequest } from '../graph-client.js';
|
|
11
|
+
const VALID_ENCODINGS = ['text', 'base64'];
|
|
12
|
+
const VALID_CONFLICT_BEHAVIOURS = ['replace', 'rename', 'fail'];
|
|
13
|
+
const DEFAULT_PARENT_ITEM_ID = 'root';
|
|
14
|
+
const MAX_UPLOAD_BYTES = 4 * 1024 * 1024;
|
|
15
|
+
export default async function execute(params, context) {
|
|
16
|
+
requireParams(params, ['drive_id', 'filename', 'content'], 'ms_create_document');
|
|
17
|
+
const driveId = String(params.drive_id);
|
|
18
|
+
const parentItemId = typeof params.parent_item_id === 'string' && params.parent_item_id
|
|
19
|
+
? params.parent_item_id
|
|
20
|
+
: DEFAULT_PARENT_ITEM_ID;
|
|
21
|
+
const filename = String(params.filename);
|
|
22
|
+
const encoding = params.content_encoding === undefined ? 'text' : String(params.content_encoding);
|
|
23
|
+
if (!VALID_ENCODINGS.includes(encoding)) {
|
|
24
|
+
throw new MatimoError(`ms_create_document: 'content_encoding' must be one of ${VALID_ENCODINGS.join(', ')} (received '${encoding}')`, ErrorCode.VALIDATION_FAILED, { content_encoding: params.content_encoding });
|
|
25
|
+
}
|
|
26
|
+
const conflictBehaviour = params.conflict_behaviour === undefined ? 'replace' : String(params.conflict_behaviour);
|
|
27
|
+
if (!VALID_CONFLICT_BEHAVIOURS.includes(conflictBehaviour)) {
|
|
28
|
+
throw new MatimoError(`ms_create_document: 'conflict_behaviour' must be one of ${VALID_CONFLICT_BEHAVIOURS.join(', ')} (received '${conflictBehaviour}')`, ErrorCode.VALIDATION_FAILED, { conflict_behaviour: params.conflict_behaviour });
|
|
29
|
+
}
|
|
30
|
+
const buffer = encoding === 'base64'
|
|
31
|
+
? Buffer.from(String(params.content), 'base64')
|
|
32
|
+
: Buffer.from(String(params.content), 'utf-8');
|
|
33
|
+
if (buffer.byteLength > MAX_UPLOAD_BYTES) {
|
|
34
|
+
throw new MatimoError(`ms_create_document: content is ${buffer.byteLength} bytes, exceeding the ` +
|
|
35
|
+
`${MAX_UPLOAD_BYTES}-byte limit of the simple-upload endpoint. Files this large ` +
|
|
36
|
+
'require a resumable upload session, which this tool does not implement.', ErrorCode.VALIDATION_FAILED, { sizeBytes: buffer.byteLength, maxBytes: MAX_UPLOAD_BYTES });
|
|
37
|
+
}
|
|
38
|
+
const token = getAccessToken(context);
|
|
39
|
+
// By-path addressing uses literal colons as delimiters — only the path SEGMENTS
|
|
40
|
+
// (drive id, parent item id, filename) are percent-encoded, not the colons.
|
|
41
|
+
const path = `/drives/${encodeURIComponent(driveId)}/items/${encodeURIComponent(parentItemId)}` +
|
|
42
|
+
`:/${encodeURIComponent(filename)}:/content`;
|
|
43
|
+
const item = await graphRequest({
|
|
44
|
+
method: 'PUT',
|
|
45
|
+
path,
|
|
46
|
+
token,
|
|
47
|
+
resourceType: 'Drive folder',
|
|
48
|
+
query: { '@microsoft.graph.conflictBehavior': conflictBehaviour },
|
|
49
|
+
body: buffer,
|
|
50
|
+
headers: { 'Content-Type': 'application/octet-stream' },
|
|
51
|
+
});
|
|
52
|
+
return {
|
|
53
|
+
success: true,
|
|
54
|
+
item_id: item?.id ?? '',
|
|
55
|
+
name: item?.name ?? filename,
|
|
56
|
+
web_url: item?.webUrl ?? '',
|
|
57
|
+
size_bytes: item?.size ?? buffer.byteLength,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
@@ -6,8 +6,12 @@
|
|
|
6
6
|
* 4 MB; larger files require a resumable upload session, which is out of scope here
|
|
7
7
|
* and is rejected with a clear validation error rather than silently truncating.
|
|
8
8
|
*/
|
|
9
|
-
import { MatimoError, ErrorCode } from '@matimo/core';
|
|
10
|
-
import { getAccessToken, requireParams, graphRequest
|
|
9
|
+
import { MatimoError, ErrorCode } from '@matimo/core/runtime';
|
|
10
|
+
import { getAccessToken, requireParams, graphRequest } from '../graph-client.js';
|
|
11
|
+
|
|
12
|
+
interface ToolContext {
|
|
13
|
+
credentials?: Record<string, string>;
|
|
14
|
+
}
|
|
11
15
|
|
|
12
16
|
const VALID_ENCODINGS = ['text', 'base64'];
|
|
13
17
|
const VALID_CONFLICT_BEHAVIOURS = ['replace', 'rename', 'fail'];
|
|
@@ -0,0 +1,56 @@
|
|
|
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/runtime';
|
|
6
|
+
import { getAccessToken, requireParams, graphRequest } from '../graph-client.js';
|
|
7
|
+
const DEFAULT_TOP = 10;
|
|
8
|
+
const MAX_TOP = 50;
|
|
9
|
+
function formatSender(message) {
|
|
10
|
+
const address = message.from?.emailAddress;
|
|
11
|
+
if (!address)
|
|
12
|
+
return '';
|
|
13
|
+
if (address.name && address.address)
|
|
14
|
+
return `${address.name} <${address.address}>`;
|
|
15
|
+
return address.name ?? address.address ?? '';
|
|
16
|
+
}
|
|
17
|
+
export default async function execute(params, context) {
|
|
18
|
+
requireParams(params, [], 'ms_get_email');
|
|
19
|
+
const top = params.top === undefined ? DEFAULT_TOP : Number(params.top);
|
|
20
|
+
if (!Number.isFinite(top) || top < 1 || top > MAX_TOP) {
|
|
21
|
+
throw new MatimoError(`ms_get_email: 'top' must be a number between 1 and ${MAX_TOP} (received ${String(params.top)})`, ErrorCode.VALIDATION_FAILED, { top: params.top });
|
|
22
|
+
}
|
|
23
|
+
const folderId = typeof params.folder_id === 'string' && params.folder_id ? params.folder_id : undefined;
|
|
24
|
+
const filter = typeof params.filter === 'string' && params.filter ? params.filter : undefined;
|
|
25
|
+
const search = typeof params.search === 'string' && params.search ? params.search : undefined;
|
|
26
|
+
const token = getAccessToken(context);
|
|
27
|
+
const path = folderId
|
|
28
|
+
? `/me/mailFolders/${encodeURIComponent(folderId)}/messages`
|
|
29
|
+
: '/me/messages';
|
|
30
|
+
const data = await graphRequest({
|
|
31
|
+
method: 'GET',
|
|
32
|
+
path,
|
|
33
|
+
token,
|
|
34
|
+
resourceType: 'Mail folder',
|
|
35
|
+
headers: search ? { 'ConsistencyLevel': 'eventual' } : undefined,
|
|
36
|
+
query: {
|
|
37
|
+
$top: top,
|
|
38
|
+
$select: 'id,subject,from,receivedDateTime,isRead,bodyPreview,hasAttachments',
|
|
39
|
+
...(filter ? { $filter: filter } : {}),
|
|
40
|
+
...(search ? { $search: search } : {}),
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
const messages = (data?.value ?? []).map((message) => ({
|
|
44
|
+
id: message.id ?? '',
|
|
45
|
+
subject: message.subject ?? '',
|
|
46
|
+
from: formatSender(message),
|
|
47
|
+
received_at: message.receivedDateTime ?? '',
|
|
48
|
+
is_read: message.isRead ?? false,
|
|
49
|
+
body_preview: message.bodyPreview ?? '',
|
|
50
|
+
has_attachments: message.hasAttachments ?? false,
|
|
51
|
+
}));
|
|
52
|
+
return {
|
|
53
|
+
success: true,
|
|
54
|
+
messages,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
@@ -2,8 +2,12 @@
|
|
|
2
2
|
* ms_get_email — GET /me/messages
|
|
3
3
|
* https://learn.microsoft.com/en-us/graph/api/user-list-messages
|
|
4
4
|
*/
|
|
5
|
-
import { MatimoError, ErrorCode } from '@matimo/core';
|
|
6
|
-
import { getAccessToken, requireParams, graphRequest
|
|
5
|
+
import { MatimoError, ErrorCode } from '@matimo/core/runtime';
|
|
6
|
+
import { getAccessToken, requireParams, graphRequest } from '../graph-client.js';
|
|
7
|
+
|
|
8
|
+
interface ToolContext {
|
|
9
|
+
credentials?: Record<string, string>;
|
|
10
|
+
}
|
|
7
11
|
|
|
8
12
|
const DEFAULT_TOP = 10;
|
|
9
13
|
const MAX_TOP = 50;
|
|
@@ -0,0 +1,42 @@
|
|
|
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/runtime';
|
|
6
|
+
import { getAccessToken, requireParams, graphRequest } from '../graph-client.js';
|
|
7
|
+
const DEFAULT_ITEM_ID = 'root';
|
|
8
|
+
const DEFAULT_TOP = 20;
|
|
9
|
+
const MAX_TOP = 100;
|
|
10
|
+
export default async function execute(params, context) {
|
|
11
|
+
requireParams(params, ['drive_id'], 'ms_list_files');
|
|
12
|
+
const driveId = String(params.drive_id);
|
|
13
|
+
const itemId = typeof params.item_id === 'string' && params.item_id ? params.item_id : DEFAULT_ITEM_ID;
|
|
14
|
+
const top = params.top === undefined ? DEFAULT_TOP : Number(params.top);
|
|
15
|
+
if (!Number.isFinite(top) || top < 1 || top > MAX_TOP) {
|
|
16
|
+
throw new MatimoError(`ms_list_files: 'top' must be a number between 1 and ${MAX_TOP} (received ${String(params.top)})`, ErrorCode.VALIDATION_FAILED, { top: params.top });
|
|
17
|
+
}
|
|
18
|
+
const token = getAccessToken(context);
|
|
19
|
+
const data = await graphRequest({
|
|
20
|
+
method: 'GET',
|
|
21
|
+
path: `/drives/${encodeURIComponent(driveId)}/items/${encodeURIComponent(itemId)}/children`,
|
|
22
|
+
token,
|
|
23
|
+
resourceType: 'Drive folder',
|
|
24
|
+
query: {
|
|
25
|
+
$top: top,
|
|
26
|
+
$select: 'id,name,size,lastModifiedDateTime,webUrl,file,folder',
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
const items = (data?.value ?? []).map((item) => ({
|
|
30
|
+
id: item.id ?? '',
|
|
31
|
+
name: item.name ?? '',
|
|
32
|
+
type: item.folder ? 'folder' : 'file',
|
|
33
|
+
size_bytes: item.size ?? 0,
|
|
34
|
+
last_modified: item.lastModifiedDateTime ?? '',
|
|
35
|
+
...(item.file?.mimeType ? { mime_type: item.file.mimeType } : {}),
|
|
36
|
+
web_url: item.webUrl ?? '',
|
|
37
|
+
}));
|
|
38
|
+
return {
|
|
39
|
+
success: true,
|
|
40
|
+
items,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
@@ -2,8 +2,12 @@
|
|
|
2
2
|
* ms_list_files — GET /drives/{drive_id}/items/{item_id}/children
|
|
3
3
|
* https://learn.microsoft.com/en-us/graph/api/driveitem-list-children
|
|
4
4
|
*/
|
|
5
|
-
import { MatimoError, ErrorCode } from '@matimo/core';
|
|
6
|
-
import { getAccessToken, requireParams, graphRequest
|
|
5
|
+
import { MatimoError, ErrorCode } from '@matimo/core/runtime';
|
|
6
|
+
import { getAccessToken, requireParams, graphRequest } from '../graph-client.js';
|
|
7
|
+
|
|
8
|
+
interface ToolContext {
|
|
9
|
+
credentials?: Record<string, string>;
|
|
10
|
+
}
|
|
7
11
|
|
|
8
12
|
const DEFAULT_ITEM_ID = 'root';
|
|
9
13
|
const DEFAULT_TOP = 20;
|
|
@@ -0,0 +1,96 @@
|
|
|
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/runtime';
|
|
12
|
+
import { getAccessToken, requireParams, graphRequest } from '../graph-client.js';
|
|
13
|
+
const VALID_CONTENT_TYPES = ['html', 'text'];
|
|
14
|
+
const HTML_ESCAPES = {
|
|
15
|
+
'&': '&',
|
|
16
|
+
'<': '<',
|
|
17
|
+
'>': '>',
|
|
18
|
+
'"': '"',
|
|
19
|
+
"'": ''',
|
|
20
|
+
};
|
|
21
|
+
function escapeHtml(text) {
|
|
22
|
+
return text.replace(/[&<>"']/g, (ch) => HTML_ESCAPES[ch]);
|
|
23
|
+
}
|
|
24
|
+
function deriveFileName(title) {
|
|
25
|
+
const slug = title
|
|
26
|
+
.toLowerCase()
|
|
27
|
+
.trim()
|
|
28
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
29
|
+
.replace(/^-+|-+$/g, '');
|
|
30
|
+
return `${slug || 'page'}.aspx`;
|
|
31
|
+
}
|
|
32
|
+
export default async function execute(params, context) {
|
|
33
|
+
requireParams(params, ['site_id', 'title', 'content'], 'ms_publish_to_sharepoint');
|
|
34
|
+
const siteId = String(params.site_id);
|
|
35
|
+
const title = String(params.title);
|
|
36
|
+
const contentType = params.content_type === undefined ? 'html' : String(params.content_type);
|
|
37
|
+
if (!VALID_CONTENT_TYPES.includes(contentType)) {
|
|
38
|
+
throw new MatimoError(`ms_publish_to_sharepoint: 'content_type' must be one of ${VALID_CONTENT_TYPES.join(', ')} (received '${contentType}')`, ErrorCode.VALIDATION_FAILED, { content_type: params.content_type });
|
|
39
|
+
}
|
|
40
|
+
const rawContent = String(params.content);
|
|
41
|
+
const innerHtml = contentType === 'text' ? `<p>${escapeHtml(rawContent)}</p>` : rawContent;
|
|
42
|
+
const shouldPublish = params.publish === undefined ? true : params.publish === true;
|
|
43
|
+
const token = getAccessToken(context);
|
|
44
|
+
const page = await graphRequest({
|
|
45
|
+
method: 'POST',
|
|
46
|
+
path: `/sites/${encodeURIComponent(siteId)}/pages`,
|
|
47
|
+
token,
|
|
48
|
+
resourceType: 'SharePoint site',
|
|
49
|
+
body: {
|
|
50
|
+
'@odata.type': '#microsoft.graph.sitePage',
|
|
51
|
+
name: deriveFileName(title),
|
|
52
|
+
title,
|
|
53
|
+
pageLayout: 'article',
|
|
54
|
+
canvasLayout: {
|
|
55
|
+
horizontalSections: [
|
|
56
|
+
{
|
|
57
|
+
layout: 'oneColumn',
|
|
58
|
+
id: '1',
|
|
59
|
+
emphasis: 'none',
|
|
60
|
+
columns: [
|
|
61
|
+
{
|
|
62
|
+
id: '1',
|
|
63
|
+
width: 12,
|
|
64
|
+
webparts: [
|
|
65
|
+
{
|
|
66
|
+
'@odata.type': '#microsoft.graph.textWebPart',
|
|
67
|
+
innerHtml,
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
},
|
|
71
|
+
],
|
|
72
|
+
},
|
|
73
|
+
],
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
const pageId = page?.id;
|
|
78
|
+
if (!pageId) {
|
|
79
|
+
throw new MatimoError('ms_publish_to_sharepoint: Microsoft Graph did not return an ID for the created page.', ErrorCode.EXECUTION_FAILED, { page });
|
|
80
|
+
}
|
|
81
|
+
if (shouldPublish) {
|
|
82
|
+
await graphRequest({
|
|
83
|
+
method: 'POST',
|
|
84
|
+
path: `/sites/${encodeURIComponent(siteId)}/pages/${encodeURIComponent(pageId)}/microsoft.graph.sitePage/publish`,
|
|
85
|
+
token,
|
|
86
|
+
resourceType: 'SharePoint page',
|
|
87
|
+
allowEmptyResponse: true,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
success: true,
|
|
92
|
+
page_id: pageId,
|
|
93
|
+
web_url: page?.webUrl ?? '',
|
|
94
|
+
published: shouldPublish,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
@@ -8,8 +8,12 @@
|
|
|
8
8
|
* Site pages always store web part bodies as HTML, so plain-text content is
|
|
9
9
|
* HTML-escaped and wrapped in a single <p> before being placed in a textWebPart.
|
|
10
10
|
*/
|
|
11
|
-
import { MatimoError, ErrorCode } from '@matimo/core';
|
|
12
|
-
import { getAccessToken, requireParams, graphRequest
|
|
11
|
+
import { MatimoError, ErrorCode } from '@matimo/core/runtime';
|
|
12
|
+
import { getAccessToken, requireParams, graphRequest } from '../graph-client.js';
|
|
13
|
+
|
|
14
|
+
interface ToolContext {
|
|
15
|
+
credentials?: Record<string, string>;
|
|
16
|
+
}
|
|
13
17
|
|
|
14
18
|
const VALID_CONTENT_TYPES = ['html', 'text'];
|
|
15
19
|
|
|
@@ -0,0 +1,79 @@
|
|
|
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 } from '../graph-client.js';
|
|
13
|
+
const TEXT_MIME_PREFIXES = ['text/'];
|
|
14
|
+
const TEXT_MIME_TYPES = new Set(['application/json', 'application/xml']);
|
|
15
|
+
const RICH_DOCUMENT_LABELS = {
|
|
16
|
+
'application/pdf': 'PDF document',
|
|
17
|
+
'application/vnd.openxmlformats-officedocument.wordprocessingml.document': 'Word document (.docx)',
|
|
18
|
+
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': 'Excel workbook (.xlsx)',
|
|
19
|
+
'application/vnd.openxmlformats-officedocument.presentationml.presentation': 'PowerPoint presentation (.pptx)',
|
|
20
|
+
'application/msword': 'Word document (.doc)',
|
|
21
|
+
'application/vnd.ms-excel': 'Excel workbook (.xls)',
|
|
22
|
+
'application/vnd.ms-powerpoint': 'PowerPoint presentation (.ppt)',
|
|
23
|
+
};
|
|
24
|
+
function isPlainTextMime(mimeType) {
|
|
25
|
+
return TEXT_MIME_TYPES.has(mimeType) || TEXT_MIME_PREFIXES.some((p) => mimeType.startsWith(p));
|
|
26
|
+
}
|
|
27
|
+
export default async function execute(params, context) {
|
|
28
|
+
requireParams(params, ['drive_id', 'item_id'], 'ms_read_file');
|
|
29
|
+
const driveId = String(params.drive_id);
|
|
30
|
+
const itemId = String(params.item_id);
|
|
31
|
+
const token = getAccessToken(context);
|
|
32
|
+
const metadata = await graphRequest({
|
|
33
|
+
method: 'GET',
|
|
34
|
+
path: `/drives/${encodeURIComponent(driveId)}/items/${encodeURIComponent(itemId)}`,
|
|
35
|
+
token,
|
|
36
|
+
resourceType: 'Drive item',
|
|
37
|
+
query: { $select: 'name,size,file' },
|
|
38
|
+
});
|
|
39
|
+
const name = metadata?.name ?? '';
|
|
40
|
+
const mimeType = metadata?.file?.mimeType ?? 'application/octet-stream';
|
|
41
|
+
const sizeBytes = metadata?.size ?? 0;
|
|
42
|
+
const raw = await graphRequest({
|
|
43
|
+
method: 'GET',
|
|
44
|
+
path: `/drives/${encodeURIComponent(driveId)}/items/${encodeURIComponent(itemId)}/content`,
|
|
45
|
+
token,
|
|
46
|
+
resourceType: 'Drive item content',
|
|
47
|
+
responseType: 'arraybuffer',
|
|
48
|
+
});
|
|
49
|
+
const buffer = Buffer.from(raw);
|
|
50
|
+
if (isPlainTextMime(mimeType)) {
|
|
51
|
+
return {
|
|
52
|
+
success: true,
|
|
53
|
+
content: buffer.toString('utf-8'),
|
|
54
|
+
name,
|
|
55
|
+
mime_type: mimeType,
|
|
56
|
+
size_bytes: sizeBytes,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
const richDocumentLabel = RICH_DOCUMENT_LABELS[mimeType];
|
|
60
|
+
if (richDocumentLabel) {
|
|
61
|
+
return {
|
|
62
|
+
success: true,
|
|
63
|
+
content: '',
|
|
64
|
+
name,
|
|
65
|
+
mime_type: mimeType,
|
|
66
|
+
size_bytes: sizeBytes,
|
|
67
|
+
warning: `${richDocumentLabel} — text extraction for this format is not implemented to avoid ` +
|
|
68
|
+
'bundling unverified parsing dependencies. Share the file via its web URL instead.',
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
success: true,
|
|
73
|
+
content: '',
|
|
74
|
+
name,
|
|
75
|
+
mime_type: mimeType,
|
|
76
|
+
size_bytes: sizeBytes,
|
|
77
|
+
warning: 'Binary file — text extraction not supported',
|
|
78
|
+
};
|
|
79
|
+
}
|
|
@@ -9,7 +9,11 @@
|
|
|
9
9
|
* pdf-parse/mammoth/xlsx/cheerio). Truly-unsupported binaries get the exact warning
|
|
10
10
|
* the tool's contract specifies: "Binary file — text extraction not supported".
|
|
11
11
|
*/
|
|
12
|
-
import { getAccessToken, requireParams, graphRequest
|
|
12
|
+
import { getAccessToken, requireParams, graphRequest } from '../graph-client.js';
|
|
13
|
+
|
|
14
|
+
interface ToolContext {
|
|
15
|
+
credentials?: Record<string, string>;
|
|
16
|
+
}
|
|
13
17
|
|
|
14
18
|
const TEXT_MIME_PREFIXES = ['text/'];
|
|
15
19
|
const TEXT_MIME_TYPES = new Set(['application/json', 'application/xml']);
|
|
@@ -0,0 +1,63 @@
|
|
|
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/runtime';
|
|
6
|
+
import { getAccessToken, requireParams, graphRequest } from '../graph-client.js';
|
|
7
|
+
const VALID_ENTITY_TYPES = ['driveItem', 'listItem', 'site', 'list', 'drive'];
|
|
8
|
+
const DEFAULT_ENTITY_TYPES = ['driveItem', 'listItem', 'site'];
|
|
9
|
+
const DEFAULT_TOP = 10;
|
|
10
|
+
const MAX_TOP = 25;
|
|
11
|
+
export default async function execute(params, context) {
|
|
12
|
+
requireParams(params, ['query'], 'ms_search_knowledge');
|
|
13
|
+
const query = String(params.query);
|
|
14
|
+
const entityTypes = Array.isArray(params.entity_types)
|
|
15
|
+
? params.entity_types.map(String)
|
|
16
|
+
: DEFAULT_ENTITY_TYPES;
|
|
17
|
+
const invalidEntityTypes = entityTypes.filter((t) => !VALID_ENTITY_TYPES.includes(t));
|
|
18
|
+
if (entityTypes.length === 0 || invalidEntityTypes.length > 0) {
|
|
19
|
+
throw new MatimoError(`ms_search_knowledge: invalid entity_types ${JSON.stringify(invalidEntityTypes)}. ` +
|
|
20
|
+
`Valid values are: ${VALID_ENTITY_TYPES.join(', ')}`, ErrorCode.VALIDATION_FAILED, { entityTypes, invalidEntityTypes });
|
|
21
|
+
}
|
|
22
|
+
const top = params.top === undefined ? DEFAULT_TOP : Number(params.top);
|
|
23
|
+
if (!Number.isFinite(top) || top < 1 || top > MAX_TOP) {
|
|
24
|
+
throw new MatimoError(`ms_search_knowledge: 'top' must be a number between 1 and ${MAX_TOP} (received ${String(params.top)})`, ErrorCode.VALIDATION_FAILED, { top: params.top });
|
|
25
|
+
}
|
|
26
|
+
// Microsoft Search has no dedicated site/drive filter for driveItem/listItem/site
|
|
27
|
+
// entity types — fold the IDs into the query string as a best-effort scoping hint.
|
|
28
|
+
// This is documented in the tool description so callers don't expect a hard filter.
|
|
29
|
+
const scopeHints = [params.site_id, params.drive_id].filter((value) => typeof value === 'string' && value.length > 0);
|
|
30
|
+
const queryString = scopeHints.length > 0 ? `${query} ${scopeHints.join(' ')}` : query;
|
|
31
|
+
const token = getAccessToken(context);
|
|
32
|
+
const data = await graphRequest({
|
|
33
|
+
method: 'POST',
|
|
34
|
+
path: '/search/query',
|
|
35
|
+
token,
|
|
36
|
+
resourceType: 'Search results',
|
|
37
|
+
body: {
|
|
38
|
+
requests: [
|
|
39
|
+
{
|
|
40
|
+
entityTypes,
|
|
41
|
+
query: { queryString },
|
|
42
|
+
from: 0,
|
|
43
|
+
size: top,
|
|
44
|
+
},
|
|
45
|
+
],
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
const container = data?.value?.[0]?.hitsContainers?.[0];
|
|
49
|
+
const hits = container?.hits ?? [];
|
|
50
|
+
const results = hits.map((hit) => ({
|
|
51
|
+
id: hit.resource?.id ?? hit.hitId ?? '',
|
|
52
|
+
name: hit.resource?.name ?? '',
|
|
53
|
+
summary: hit.summary ?? '',
|
|
54
|
+
web_url: hit.resource?.webUrl ?? '',
|
|
55
|
+
last_modified: hit.resource?.lastModifiedDateTime ?? '',
|
|
56
|
+
score: hit.rank ?? 0,
|
|
57
|
+
}));
|
|
58
|
+
return {
|
|
59
|
+
success: true,
|
|
60
|
+
results,
|
|
61
|
+
total_count: container?.total ?? results.length,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
@@ -2,8 +2,12 @@
|
|
|
2
2
|
* ms_search_knowledge — POST /search/query
|
|
3
3
|
* https://learn.microsoft.com/en-us/graph/api/search-query
|
|
4
4
|
*/
|
|
5
|
-
import { MatimoError, ErrorCode } from '@matimo/core';
|
|
6
|
-
import { getAccessToken, requireParams, graphRequest
|
|
5
|
+
import { MatimoError, ErrorCode } from '@matimo/core/runtime';
|
|
6
|
+
import { getAccessToken, requireParams, graphRequest } from '../graph-client.js';
|
|
7
|
+
|
|
8
|
+
interface ToolContext {
|
|
9
|
+
credentials?: Record<string, string>;
|
|
10
|
+
}
|
|
7
11
|
|
|
8
12
|
const VALID_ENTITY_TYPES = ['driveItem', 'listItem', 'site', 'list', 'drive'];
|
|
9
13
|
const DEFAULT_ENTITY_TYPES = ['driveItem', 'listItem', 'site'];
|
|
@@ -0,0 +1,66 @@
|
|
|
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/runtime';
|
|
11
|
+
import { getAccessToken, requireParams, graphRequest } from '../graph-client.js';
|
|
12
|
+
const VALID_BODY_TYPES = ['text', 'html'];
|
|
13
|
+
function toRecipientList(value, fieldName) {
|
|
14
|
+
if (value === undefined)
|
|
15
|
+
return [];
|
|
16
|
+
if (!Array.isArray(value) || value.some((entry) => typeof entry !== 'string' || !entry)) {
|
|
17
|
+
throw new MatimoError(`ms_send_email: '${fieldName}' must be an array of email address strings`, ErrorCode.VALIDATION_FAILED, { field: fieldName, received: value });
|
|
18
|
+
}
|
|
19
|
+
return value.map((address) => ({ emailAddress: { address } }));
|
|
20
|
+
}
|
|
21
|
+
export default async function execute(params, context) {
|
|
22
|
+
requireParams(params, ['to', 'subject', 'body'], 'ms_send_email');
|
|
23
|
+
const to = toRecipientList(params.to, 'to');
|
|
24
|
+
if (to.length === 0) {
|
|
25
|
+
throw new MatimoError("ms_send_email: 'to' must contain at least one recipient email address", ErrorCode.VALIDATION_FAILED, { to: params.to });
|
|
26
|
+
}
|
|
27
|
+
const cc = toRecipientList(params.cc, 'cc');
|
|
28
|
+
const bcc = toRecipientList(params.bcc, 'bcc');
|
|
29
|
+
const bodyType = params.body_type === undefined ? 'text' : String(params.body_type);
|
|
30
|
+
if (!VALID_BODY_TYPES.includes(bodyType)) {
|
|
31
|
+
throw new MatimoError(`ms_send_email: 'body_type' must be one of ${VALID_BODY_TYPES.join(', ')} (received '${bodyType}')`, ErrorCode.VALIDATION_FAILED, { body_type: params.body_type });
|
|
32
|
+
}
|
|
33
|
+
const token = getAccessToken(context);
|
|
34
|
+
const draft = await graphRequest({
|
|
35
|
+
method: 'POST',
|
|
36
|
+
path: '/me/messages',
|
|
37
|
+
token,
|
|
38
|
+
resourceType: 'Mail draft',
|
|
39
|
+
body: {
|
|
40
|
+
subject: String(params.subject),
|
|
41
|
+
body: {
|
|
42
|
+
contentType: bodyType === 'html' ? 'HTML' : 'Text',
|
|
43
|
+
content: String(params.body),
|
|
44
|
+
},
|
|
45
|
+
toRecipients: to,
|
|
46
|
+
...(cc.length > 0 ? { ccRecipients: cc } : {}),
|
|
47
|
+
...(bcc.length > 0 ? { bccRecipients: bcc } : {}),
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
const messageId = draft?.id;
|
|
51
|
+
if (!messageId) {
|
|
52
|
+
throw new MatimoError('ms_send_email: Microsoft Graph did not return an ID for the created draft message.', ErrorCode.EXECUTION_FAILED, { draft });
|
|
53
|
+
}
|
|
54
|
+
await graphRequest({
|
|
55
|
+
method: 'POST',
|
|
56
|
+
path: `/me/messages/${encodeURIComponent(messageId)}/send`,
|
|
57
|
+
token,
|
|
58
|
+
resourceType: 'Mail draft',
|
|
59
|
+
allowEmptyResponse: true,
|
|
60
|
+
});
|
|
61
|
+
return {
|
|
62
|
+
success: true,
|
|
63
|
+
sent: true,
|
|
64
|
+
message_id: messageId,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
@@ -7,8 +7,12 @@
|
|
|
7
7
|
* identifier, but this tool's contract promises a `message_id`. Creating a draft
|
|
8
8
|
* first gives us a real message ID we can report back, then we send that draft.
|
|
9
9
|
*/
|
|
10
|
-
import { MatimoError, ErrorCode } from '@matimo/core';
|
|
11
|
-
import { getAccessToken, requireParams, graphRequest
|
|
10
|
+
import { MatimoError, ErrorCode } from '@matimo/core/runtime';
|
|
11
|
+
import { getAccessToken, requireParams, graphRequest } from '../graph-client.js';
|
|
12
|
+
|
|
13
|
+
interface ToolContext {
|
|
14
|
+
credentials?: Record<string, string>;
|
|
15
|
+
}
|
|
12
16
|
|
|
13
17
|
const VALID_BODY_TYPES = ['text', 'html'];
|
|
14
18
|
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ms_send_teams_message
|
|
3
|
+
* New message: POST /teams/{team-id}/channels/{channel-id}/messages
|
|
4
|
+
* https://learn.microsoft.com/en-us/graph/api/channel-post-messages
|
|
5
|
+
* Reply: POST /teams/{team-id}/channels/{channel-id}/messages/{message-id}/replies
|
|
6
|
+
* https://learn.microsoft.com/en-us/graph/api/chatmessage-post-replies
|
|
7
|
+
*/
|
|
8
|
+
import { MatimoError, ErrorCode } from '@matimo/core/runtime';
|
|
9
|
+
import { getAccessToken, requireParams, graphRequest } from '../graph-client.js';
|
|
10
|
+
const VALID_CONTENT_TYPES = ['text', 'html'];
|
|
11
|
+
export default async function execute(params, context) {
|
|
12
|
+
requireParams(params, ['team_id', 'channel_id', 'text'], 'ms_send_teams_message');
|
|
13
|
+
const teamId = String(params.team_id);
|
|
14
|
+
const channelId = String(params.channel_id);
|
|
15
|
+
const text = String(params.text);
|
|
16
|
+
const contentType = params.content_type === undefined ? 'text' : String(params.content_type);
|
|
17
|
+
if (!VALID_CONTENT_TYPES.includes(contentType)) {
|
|
18
|
+
throw new MatimoError(`ms_send_teams_message: 'content_type' must be one of ${VALID_CONTENT_TYPES.join(', ')} (received '${contentType}')`, ErrorCode.VALIDATION_FAILED, { content_type: params.content_type });
|
|
19
|
+
}
|
|
20
|
+
const replyToMessageId = typeof params.reply_to_message_id === 'string' && params.reply_to_message_id
|
|
21
|
+
? params.reply_to_message_id
|
|
22
|
+
: undefined;
|
|
23
|
+
const token = getAccessToken(context);
|
|
24
|
+
const basePath = `/teams/${encodeURIComponent(teamId)}/channels/${encodeURIComponent(channelId)}/messages`;
|
|
25
|
+
const path = replyToMessageId
|
|
26
|
+
? `${basePath}/${encodeURIComponent(replyToMessageId)}/replies`
|
|
27
|
+
: basePath;
|
|
28
|
+
const message = await graphRequest({
|
|
29
|
+
method: 'POST',
|
|
30
|
+
path,
|
|
31
|
+
token,
|
|
32
|
+
resourceType: 'Teams channel',
|
|
33
|
+
body: {
|
|
34
|
+
body: {
|
|
35
|
+
contentType,
|
|
36
|
+
content: text,
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
return {
|
|
41
|
+
success: true,
|
|
42
|
+
message_id: message?.id ?? '',
|
|
43
|
+
web_url: message?.webUrl ?? '',
|
|
44
|
+
created_at: message?.createdDateTime ?? '',
|
|
45
|
+
};
|
|
46
|
+
}
|
|
@@ -5,8 +5,12 @@
|
|
|
5
5
|
* Reply: POST /teams/{team-id}/channels/{channel-id}/messages/{message-id}/replies
|
|
6
6
|
* https://learn.microsoft.com/en-us/graph/api/chatmessage-post-replies
|
|
7
7
|
*/
|
|
8
|
-
import { MatimoError, ErrorCode } from '@matimo/core';
|
|
9
|
-
import { getAccessToken, requireParams, graphRequest
|
|
8
|
+
import { MatimoError, ErrorCode } from '@matimo/core/runtime';
|
|
9
|
+
import { getAccessToken, requireParams, graphRequest } from '../graph-client.js';
|
|
10
|
+
|
|
11
|
+
interface ToolContext {
|
|
12
|
+
credentials?: Record<string, string>;
|
|
13
|
+
}
|
|
10
14
|
|
|
11
15
|
const VALID_CONTENT_TYPES = ['text', 'html'];
|
|
12
16
|
|