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