@typeroll/mcp-server 0.45.43 → 0.45.45
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/AGENTS.md +155 -43
- package/README.md +87 -23
- package/dist/bundled-content.js +9 -9
- package/dist/client.js +15 -1
- package/dist/compact-tools.js +1 -1
- package/dist/server.js +49 -2
- package/dist/tools/access.js +75 -0
- package/dist/tools/content-types.js +2 -2
- package/dist/tools/domain.js +33 -0
- package/dist/tools/email.js +69 -0
- package/dist/tools/extensions.js +131 -0
- package/dist/tools/forms.js +34 -4
- package/dist/tools/helpers.js +7 -0
- package/dist/tools/media.js +12 -0
- package/dist/tools/page-blocks.js +5 -33
- package/dist/tools/pages.js +11 -4
- package/dist/tools/partials.js +1 -1
- package/dist/tools/reusable-blocks.js +1 -1
- package/dist/tools/settings.js +20 -10
- package/dist/tools/sites.js +25 -3
- package/dist/tools/versions.js +21 -3
- package/dist/tools/workflows.js +61 -0
- package/dist/tools/working-copy.js +67 -3
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/skills/tr-content-write.md +3 -3
- package/skills/tr-forms.md +36 -8
- package/skills/tr-import-url.md +2 -2
- package/skills/tr-migrate-wp.md +9 -5
- package/skills/tr-new-site.md +5 -4
- package/skills/tr-redesign-branch.md +15 -1
- package/skills/tr-seo.md +14 -0
package/dist/client.js
CHANGED
|
@@ -44,7 +44,7 @@ export class TyperollClient {
|
|
|
44
44
|
}
|
|
45
45
|
return url.toString();
|
|
46
46
|
}
|
|
47
|
-
/** Top-level URL (no siteId prefix) —
|
|
47
|
+
/** Top-level URL (no siteId prefix) — /v1/sites, /v1/organization/*, /v1/publishing/*. */
|
|
48
48
|
rootUrl(path, query) {
|
|
49
49
|
const cleanPath = path.replace(/^\/+/, '');
|
|
50
50
|
const url = new URL(`${this.baseUrl}/api/v1/${cleanPath}`);
|
|
@@ -110,4 +110,18 @@ export class TyperollClient {
|
|
|
110
110
|
rootPut(path, body) {
|
|
111
111
|
return this.request('PUT', this.rootUrl(path), body);
|
|
112
112
|
}
|
|
113
|
+
// ── Extension developer API ────────────────────────────────────────────
|
|
114
|
+
/** `/api/developer/{path}` — the Extension developer API used by the
|
|
115
|
+
* `typeroll extension` CLI. Requires an organization-scoped key. */
|
|
116
|
+
developer(method, path, body, query) {
|
|
117
|
+
const url = new URL(`${this.baseUrl}/api/developer/${path.replace(/^\/+/, '')}`);
|
|
118
|
+
for (const [k, v] of Object.entries(query ?? {})) {
|
|
119
|
+
if (v !== undefined && v !== null)
|
|
120
|
+
url.searchParams.set(k, String(v));
|
|
121
|
+
}
|
|
122
|
+
return this.request(method, url.toString(), body);
|
|
123
|
+
}
|
|
124
|
+
rootDelete(path, body) {
|
|
125
|
+
return this.request('DELETE', this.rootUrl(path), body);
|
|
126
|
+
}
|
|
113
127
|
}
|
package/dist/compact-tools.js
CHANGED
|
@@ -22,7 +22,7 @@ export function compactTools(catalog) {
|
|
|
22
22
|
}),
|
|
23
23
|
...['read', 'write', 'admin'].map(effect => ({
|
|
24
24
|
name: `call_${effect}_tool`, effect,
|
|
25
|
-
description: effect === 'read' ? 'Call a discovered read-only tool with its exact arguments. Does not accept write/admin tools.' : `Call a discovered ${effect} tool. May ${effect === 'admin' ? 'change app configuration or activate releases' : 'save, delete, publish or otherwise change state'}. Inspect describe_tool first and follow the user’s authorization.`,
|
|
25
|
+
description: effect === 'read' ? 'Call a discovered read-only tool with its exact arguments. Does not accept write/admin tools.' : `Call a discovered ${effect} tool. May ${effect === 'admin' ? 'change app or email configuration, send a test email or activate releases' : 'save, delete, publish or otherwise change state'}. Inspect describe_tool first and follow the user’s authorization.`,
|
|
26
26
|
schema: { name: z.string().max(100), arguments: z.record(z.unknown()).default({}) },
|
|
27
27
|
invoke: async (args) => {
|
|
28
28
|
const tool = byName.get(String(args.name));
|
package/dist/server.js
CHANGED
|
@@ -20,6 +20,7 @@ import { styleTools } from './tools/styles.js';
|
|
|
20
20
|
import { reusableBlockTools } from './tools/reusable-blocks.js';
|
|
21
21
|
import { migrationTools } from './tools/migration.js';
|
|
22
22
|
import { formTools } from './tools/forms.js';
|
|
23
|
+
import { emailTools } from './tools/email.js';
|
|
23
24
|
import { searchTools } from './tools/search.js';
|
|
24
25
|
import { bulkTools } from './tools/bulk.js';
|
|
25
26
|
import { versionTools } from './tools/versions.js';
|
|
@@ -32,8 +33,10 @@ import { settingsTools } from './tools/settings.js';
|
|
|
32
33
|
import { siteTools } from './tools/sites.js';
|
|
33
34
|
import { domainTools } from './tools/domain.js';
|
|
34
35
|
import { appTools } from './tools/apps.js';
|
|
35
|
-
import { extensionTools } from './tools/extensions.js';
|
|
36
|
+
import { extensionTools, developerExtensionTools } from './tools/extensions.js';
|
|
36
37
|
import { skillTools } from './tools/skills.js';
|
|
38
|
+
import { accessTools } from './tools/access.js';
|
|
39
|
+
import { workflowTools } from './tools/workflows.js';
|
|
37
40
|
import { fail } from './tools/helpers.js';
|
|
38
41
|
import { VERSION } from './version.js';
|
|
39
42
|
import { compactTools, COMPACT_INSTRUCTIONS } from './compact-tools.js';
|
|
@@ -60,8 +63,48 @@ function effectFor(name) {
|
|
|
60
63
|
|| name === 'list_extension_installations'
|
|
61
64
|
|| name === 'read_extension_installation'
|
|
62
65
|
|| name === 'update_extension_installation_config'
|
|
63
|
-
|| name === 'activate_extension_release'
|
|
66
|
+
|| name === 'activate_extension_release'
|
|
67
|
+
// Same role as the portal: Settings, lifecycle and media purge are
|
|
68
|
+
// admin actions (the v1 routes enforce it as well).
|
|
69
|
+
|| name === 'update_site_settings'
|
|
70
|
+
|| name === 'archive_site'
|
|
71
|
+
|| name === 'restore_site'
|
|
72
|
+
|| name === 'purge_site_media'
|
|
73
|
+
|| name === 'install_extension'
|
|
74
|
+
|| name === 'set_extension_installation_status'
|
|
75
|
+
|| name === 'uninstall_extension'
|
|
76
|
+
|| name === 'pair_extension_issuer'
|
|
77
|
+
|| name === 'read_extension_diagnostics'
|
|
78
|
+
// Branch lifecycle on main is site administration in the portal too.
|
|
79
|
+
|| name === 'create_branch'
|
|
80
|
+
|| name === 'merge_branch'
|
|
81
|
+
|| name === 'delete_branch'
|
|
82
|
+
|| name === 'reset_version'
|
|
83
|
+
// Developer-organization administration (org admin in the portal).
|
|
84
|
+
|| name === 'update_developer_extension'
|
|
85
|
+
|| name === 'save_extension_version'
|
|
86
|
+
|| name === 'publish_extension_version'
|
|
87
|
+
|| name === 'set_extension_version_lifecycle'
|
|
88
|
+
// Access and credential management: API keys, sharing, membership invites
|
|
89
|
+
// and organization publishing connections need admin, as in the portal.
|
|
90
|
+
|| name === 'revoke_api_key'
|
|
91
|
+
|| name === 'list_site_shares'
|
|
92
|
+
|| name === 'share_site'
|
|
93
|
+
|| name === 'update_site_share'
|
|
94
|
+
|| name === 'revoke_site_share'
|
|
95
|
+
|| name === 'revoke_organization_api_key'
|
|
96
|
+
|| name === 'create_organization_invite'
|
|
97
|
+
|| name === 'disconnect_organization_publishing_provider'
|
|
98
|
+
|| name === 'connect_organization_cloudflare'
|
|
99
|
+
|| name === 'prepare_organization_media_storage'
|
|
100
|
+
|| name === 'save_organization_media_access'
|
|
101
|
+
|| name === 'set_email_settings'
|
|
102
|
+
|| name === 'delete_email_settings'
|
|
103
|
+
|| name === 'send_test_email'
|
|
104
|
+
|| name === 'set_incoming_email_forwarding')
|
|
64
105
|
return 'admin';
|
|
106
|
+
if (name === 'diff_version')
|
|
107
|
+
return 'read';
|
|
65
108
|
if (name.startsWith('list_') ||
|
|
66
109
|
name.startsWith('read_') ||
|
|
67
110
|
name.startsWith('get_') ||
|
|
@@ -158,15 +201,19 @@ export function buildServer(options) {
|
|
|
158
201
|
...reusableBlockTools,
|
|
159
202
|
...migrationTools,
|
|
160
203
|
...formTools,
|
|
204
|
+
...emailTools,
|
|
161
205
|
...settingsTools,
|
|
162
206
|
...appTools,
|
|
163
207
|
...extensionTools,
|
|
208
|
+
...developerExtensionTools,
|
|
164
209
|
...searchTools,
|
|
165
210
|
...bulkTools,
|
|
166
211
|
...versionTools,
|
|
167
212
|
...deployTools,
|
|
168
213
|
...previewTools,
|
|
169
214
|
...domainTools,
|
|
215
|
+
...workflowTools,
|
|
216
|
+
...accessTools,
|
|
170
217
|
];
|
|
171
218
|
// SDK's registerTool has deeply-nested generics we can't unify across a
|
|
172
219
|
// heterogeneous tool list at compile time — same shrug as stdio's index.ts.
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// Access tools — API keys, cross-organization site sharing and organization
|
|
2
|
+
// invites. Each wraps the authenticated public API with the same permission
|
|
3
|
+
// checks as the portal: a key never mints a key or a grant that reaches
|
|
4
|
+
// further than it does.
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
import { ok, withErrorBoundary } from './helpers.js';
|
|
7
|
+
const keyId = z.string().regex(/^[a-f0-9]{12}$/).describe('Key id (the 12-hex prefix) from list_api_keys / list_organization_api_keys.');
|
|
8
|
+
export const accessTools = [
|
|
9
|
+
{
|
|
10
|
+
name: 'list_api_keys',
|
|
11
|
+
description: 'List this Site\'s site-scoped API keys: id (prefix), name, created_at/created_by, last_used_at/last_used_ip and revoked_at. Never returns secrets or hashes. Any key that reaches the Site may list, as in Site settings → API keys.',
|
|
12
|
+
inputSchema: {},
|
|
13
|
+
handler: withErrorBoundary(async (_args, { client, siteId }) => ok(await client.get(siteId, 'api-keys'))),
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
name: 'revoke_api_key',
|
|
17
|
+
description: 'Revoke one of this Site\'s API keys (admin permission on the Site). It stops authenticating immediately and stays listed as revoked for audit. Revoking the key this connection uses ends the connection. Organization keys are revoked with revoke_organization_api_key.',
|
|
18
|
+
inputSchema: { key_id: keyId },
|
|
19
|
+
handler: withErrorBoundary(async (args, { client, siteId }) => ok(await client.del(siteId, `api-keys/${encodeURIComponent(args.key_id)}`))),
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
name: 'list_site_shares',
|
|
23
|
+
description: 'List the organizations this Site is shared with, including revoked shares for audit: id, shared_with_org_id, permission (read | write | admin), label, created_at, revoked_at. Admin permission on the Site, as in the portal.',
|
|
24
|
+
inputSchema: {},
|
|
25
|
+
handler: withErrorBoundary(async (_args, { client, siteId }) => ok(await client.get(siteId, 'shares'))),
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
name: 'share_site',
|
|
29
|
+
description: 'Share this Site with another Organization, identified by org_id or org_slug (admin permission on the Site). permission is read, write (default) or admin; a share never exceeds admin, which the caller already holds. One active share per Organization: change it with update_site_share. Confirm the recipient with the user first — it gives every member of that Organization access.',
|
|
30
|
+
inputSchema: {
|
|
31
|
+
org_id: z.string().optional().describe('Recipient Organization id.'),
|
|
32
|
+
org_slug: z.string().optional().describe('Recipient Organization slug, if the id is not known.'),
|
|
33
|
+
permission: z.enum(['read', 'write', 'admin']).optional(),
|
|
34
|
+
label: z.string().max(200).optional().describe('Optional note shown in the sharing list.'),
|
|
35
|
+
},
|
|
36
|
+
handler: withErrorBoundary(async (args, { client, siteId }) => ok(await client.post(siteId, 'shares', args))),
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
name: 'update_site_share',
|
|
40
|
+
description: 'Change the permission (read | write | admin) and/or label of an active share of this Site (admin permission on the Site). Takes effect on the recipient\'s next request.',
|
|
41
|
+
inputSchema: {
|
|
42
|
+
share_id: z.string().describe('Share id from list_site_shares.'),
|
|
43
|
+
permission: z.enum(['read', 'write', 'admin']).optional(),
|
|
44
|
+
label: z.string().max(200).optional(),
|
|
45
|
+
},
|
|
46
|
+
handler: withErrorBoundary(async ({ share_id, ...patch }, { client, siteId }) => ok(await client.patch(siteId, `shares/${encodeURIComponent(share_id)}`, patch))),
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
name: 'revoke_site_share',
|
|
50
|
+
description: 'Revoke a share of this Site (admin permission on the Site). The recipient Organization and its keys lose access immediately; the record stays listed as revoked.',
|
|
51
|
+
inputSchema: { share_id: z.string().describe('Share id from list_site_shares.') },
|
|
52
|
+
handler: withErrorBoundary(async (args, { client, siteId }) => ok(await client.del(siteId, `shares/${encodeURIComponent(args.share_id)}`))),
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
name: 'list_organization_api_keys',
|
|
56
|
+
noSite: true,
|
|
57
|
+
description: 'List the Organization\'s organization-scoped API keys (metadata only, never secrets). Requires an organization API key; site-scoped keys get 403. Site-scoped keys are listed per Site with list_api_keys.',
|
|
58
|
+
inputSchema: {},
|
|
59
|
+
handler: withErrorBoundary(async (_args, { client }) => ok(await client.rootGet('organization/api-keys'))),
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
name: 'revoke_organization_api_key',
|
|
63
|
+
noSite: true,
|
|
64
|
+
description: 'Revoke an organization-scoped API key. Requires an organization API key. It stops authenticating immediately; revoking the key this connection uses ends the connection.',
|
|
65
|
+
inputSchema: { key_id: keyId },
|
|
66
|
+
handler: withErrorBoundary(async (args, { client }) => ok(await client.rootDelete(`organization/api-keys/${encodeURIComponent(args.key_id)}`))),
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
name: 'create_organization_invite',
|
|
70
|
+
noSite: true,
|
|
71
|
+
description: 'Create a signed invite link to the Organization (organization API key required). The invited person opens invite_url, signs in and joins as an editor; the link works until expires_at (ttl_days 1–30, default 7) and cannot be revoked early. Send it only to the person the user named.',
|
|
72
|
+
inputSchema: { ttl_days: z.number().int().min(1).max(30).optional() },
|
|
73
|
+
handler: withErrorBoundary(async (args, { client }) => ok(await client.rootPost('organization/invites', args))),
|
|
74
|
+
},
|
|
75
|
+
];
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import { ok, withErrorBoundary, versionParam } from './helpers.js';
|
|
2
|
+
import { ok, withErrorBoundary, versionParam, writeAuthority } from './helpers.js';
|
|
3
3
|
const schema = z.array(z.object({ name: z.string(), label: z.string(), type: z.enum(['text', 'textarea', 'richtext', 'image', 'file', 'color', 'select', 'multiselect', 'boolean', 'number', 'url', 'email', 'date', 'datetime', 'list', 'list_simple', 'array', 'object', 'page_ref', 'page_ref_list']) }).passthrough());
|
|
4
4
|
const fields = {
|
|
5
5
|
label_singular: z.string().optional(), label_plural: z.string().optional(), fields: schema.optional(),
|
|
@@ -14,7 +14,7 @@ const fields = {
|
|
|
14
14
|
export const contentTypeTools = [
|
|
15
15
|
{ name: 'page_completeness', description: 'Report missing, unverified and stale fields for Pages of one content type, with per-field counts and worst-first Page IDs. Read-only; agent-writable fields only by default.', inputSchema: { content_type: z.string(), limit: z.number().int().min(1).max(200).optional(), stale_after_days: z.number().min(0).optional(), agent_writable_only: z.boolean().optional(), version: versionParam },
|
|
16
16
|
handler: withErrorBoundary(async (args, { client, siteId }) => { const { content_type, ...query } = args; return ok(await client.get(siteId, `content-types/${encodeURIComponent(content_type)}/completeness`, query)); }) },
|
|
17
|
-
{ name: 'change_page_content_type', description: 'Change a saved Page to another content type. Preserves its identity, block body, status and existing URL. Save or discard its working copy first. If supplied, fields replaces the complete custom fields object; omitted fields are kept and must be valid for the new type. Snapshots a revision before changing.', inputSchema: { page_id: z.string(), content_type: z.string(), fields: z.record(z.unknown()).optional(), version: versionParam },
|
|
17
|
+
{ name: 'change_page_content_type', description: 'Change a saved Page to another content type. Preserves its identity, block body, status and existing URL. Save or discard its working copy first. If supplied, fields replaces the complete custom fields object; omitted fields are kept and must be valid for the new type. Snapshots a revision before changing.', inputSchema: { page_id: z.string(), content_type: z.string(), fields: z.record(z.unknown()).optional(), ...writeAuthority, version: versionParam },
|
|
18
18
|
handler: withErrorBoundary(async (args, { client, siteId }) => { const { page_id, version, ...body } = args; return ok(await client.post(siteId, `pages/${encodeURIComponent(page_id)}/content-type`, body, { version })); }) },
|
|
19
19
|
{ name: 'list_content_types', description: 'List the field schemas, URL patterns, sorting, allowed templates and default templates for Pages. All content is a Page; use list_pages with content_type to filter.', inputSchema: { version: versionParam },
|
|
20
20
|
handler: withErrorBoundary(async (args, { client, siteId }) => ok(await client.get(siteId, 'content-types', { version: args.version }))) },
|
package/dist/tools/domain.js
CHANGED
|
@@ -28,6 +28,39 @@ export const domainTools = [
|
|
|
28
28
|
inputSchema: { revision: z.string() },
|
|
29
29
|
handler: withErrorBoundary(async (args, { client, siteId }) => ok(await client.post(siteId, 'publishing/managed-migration', args))),
|
|
30
30
|
},
|
|
31
|
+
{
|
|
32
|
+
name: 'read_organization_publishing_connections', noSite: true,
|
|
33
|
+
description: 'Read the Organization\'s GitHub and Cloudflare publishing connections: status, revision (needed to change or disconnect), account identity, media_ready, media migration, and connect_urls. Requires an organization API key; never returns credentials. GitHub sign-in/App installation and Cloudflare OAuth need a person in a browser: give the user the matching connect_urls link (an organization owner or admin completes it), then read again.',
|
|
34
|
+
inputSchema: {},
|
|
35
|
+
handler: withErrorBoundary(async (_args, { client }) => ok(await client.rootGet('publishing/connections'))),
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
name: 'disconnect_organization_publishing_provider', noSite: true,
|
|
39
|
+
description: 'Disconnect the Organization\'s GitHub or Cloudflare publishing connection at the revision from read_organization_publishing_connections. Erases stored credentials; does not delete repositories, buckets, media, DNS or published sites, but publishing through that provider stops until reconnected. Requires an organization API key and the user\'s explicit go-ahead.',
|
|
40
|
+
inputSchema: { provider: z.enum(['github', 'cloudflare']), revision: z.string(), hosting_group_id: z.string().optional().describe('Omit for the organization (Default) connection.') },
|
|
41
|
+
handler: withErrorBoundary(async ({ provider, ...body }, { client }) => ok(await client.rootDelete(`publishing/connections/${provider}`, body))),
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
name: 'connect_organization_cloudflare', noSite: true,
|
|
45
|
+
description: 'Connect the Organization\'s default Cloudflare account without a browser, using a customer Cloudflare API token plus R2 access keys for an existing private bucket. Verifies the account, Pages access and the bucket before saving; credentials are encrypted and never returned. Requires an organization API key and the current revision. Reconnecting must keep the same account and bucket. For additional Hosting Groups use connect_hosting_group.',
|
|
46
|
+
inputSchema: {
|
|
47
|
+
revision: z.string(), account_id: z.string().regex(/^[a-f0-9]{32}$/), bucket: z.string(),
|
|
48
|
+
api_token: z.string(), access_key_id: z.string(), secret_access_key: z.string(),
|
|
49
|
+
},
|
|
50
|
+
handler: withErrorBoundary(async (args, { client }) => ok(await client.rootPost('publishing/connections/cloudflare', { ...args, action: 'connect' }))),
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
name: 'prepare_organization_media_storage', noSite: true,
|
|
54
|
+
description: 'Create or reuse the Organization\'s private and public R2 media buckets in the connected Cloudflare account and configure upload CORS and lifecycle rules. Run after Cloudflare is connected and before save_organization_media_access. Requires an organization API key and the current Cloudflare revision. Idempotent.',
|
|
55
|
+
inputSchema: { revision: z.string() },
|
|
56
|
+
handler: withErrorBoundary(async (args, { client }) => ok(await client.rootPost('publishing/connections/cloudflare', { ...args, action: 'prepare_media' }))),
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
name: 'save_organization_media_access', noSite: true,
|
|
60
|
+
description: 'Save and verify the Cloudflare R2 Access Key ID and Secret Access Key for the prepared media buckets (not the Cloudflare API token). Marks media storage ready, which imports require, and queues media migration. Requires an organization API key and the current Cloudflare revision. Keys are encrypted and never returned.',
|
|
61
|
+
inputSchema: { revision: z.string(), access_key_id: z.string(), secret_access_key: z.string() },
|
|
62
|
+
handler: withErrorBoundary(async (args, { client }) => ok(await client.rootPost('publishing/connections/cloudflare', { ...args, action: 'save_media' }))),
|
|
63
|
+
},
|
|
31
64
|
{
|
|
32
65
|
name: 'check_organization_github_permissions', noSite: true,
|
|
33
66
|
description: 'Check live GitHub build permissions without disconnecting or reconnecting. Requires an organization API key. Distinguishes an update the organization owner can approve from permissions Typeroll has not requested yet. Returns the existing installation approval URL only when actionable; does not enable a build engine.',
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// Site email — the same settings as the portal's Settings → Email & notifications, with the
|
|
2
|
+
// same admin permission: the outgoing connector that form notifications send
|
|
3
|
+
// through (with a test send), and incoming email forwarding for host-approved
|
|
4
|
+
// addresses. Secrets are write-only: reads return { set: true } instead.
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
import { ok, withErrorBoundary } from './helpers.js';
|
|
7
|
+
export const emailTools = [
|
|
8
|
+
{
|
|
9
|
+
name: 'get_email_settings',
|
|
10
|
+
description: "Read the site's outgoing email connector (the provider form notifications send through): type, from, reply_to and config with secrets masked as { set: true }, plus every available provider with its field schema and whether the server can store secrets (crypto_configured). email is null when nothing is connected. Admin permission required.",
|
|
11
|
+
handler: withErrorBoundary(async (_args, { client, siteId }) => ok(await client.get(siteId, 'integrations/email'))),
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
name: 'set_email_settings',
|
|
15
|
+
description: "Connect or change the site's outgoing email provider, like Settings → Email & notifications in the portal. Read get_email_settings first for the provider field schema. Examples: postmark config { server_token, message_stream? }; smtp config { host, port?, secure?, user?, password? }. Secrets are encrypted server-side and never returned; omit a secret (or send the masked value) to keep the stored one. Then call send_test_email. Admin permission required.",
|
|
16
|
+
inputSchema: {
|
|
17
|
+
type: z.string().min(1).describe('Provider type from get_email_settings providers, e.g. "postmark" or "smtp".'),
|
|
18
|
+
from: z.string().min(1).describe('Sender, e.g. "Acme <hello@acme.com>". Must be a sender the provider accepts.'),
|
|
19
|
+
reply_to: z.string().optional().describe('Optional default Reply-To address.'),
|
|
20
|
+
config: z.record(z.unknown()).optional().describe('Provider fields keyed by the field schema.'),
|
|
21
|
+
},
|
|
22
|
+
handler: withErrorBoundary(async (args, { client, siteId }) => ok(await client.put(siteId, 'integrations/email', {
|
|
23
|
+
type: args.type,
|
|
24
|
+
from: args.from,
|
|
25
|
+
...(args.reply_to !== undefined ? { reply_to: args.reply_to } : {}),
|
|
26
|
+
config: args.config ?? {},
|
|
27
|
+
}))),
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
name: 'delete_email_settings',
|
|
31
|
+
description: "Disconnect the site's outgoing email provider and delete its stored credentials. Form email notifications stop being sent until a provider is connected again; submissions are still stored. Admin permission required.",
|
|
32
|
+
handler: withErrorBoundary(async (_args, { client, siteId }) => ok(await client.del(siteId, 'integrations/email'))),
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
name: 'send_test_email',
|
|
36
|
+
description: 'Send a test email through the stored email connector to check it works, like "Send test" in the portal. Save the connector with set_email_settings first. Admin permission required.',
|
|
37
|
+
inputSchema: {
|
|
38
|
+
to: z.string().min(3).describe('Recipient address for the test message.'),
|
|
39
|
+
},
|
|
40
|
+
handler: withErrorBoundary(async (args, { client, siteId }) => ok(await client.post(siteId, 'integrations/email/test', { to: args.to }))),
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
name: 'get_incoming_email_settings',
|
|
44
|
+
description: "Read the site's incoming email forwarding: host-approved routes (alias → forwarding target), each route's current revision, whether forwarding is enabled and the last message receipt with delivery status. Routes are created by the hosting administrator; an empty list means none is approved yet. Admin permission required.",
|
|
45
|
+
handler: withErrorBoundary(async (_args, { client, siteId }) => ok(await client.get(siteId, 'delivery/inbound'))),
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
name: 'set_incoming_email_forwarding',
|
|
49
|
+
description: 'Enable or disable forwarding for one host-approved incoming email route, like Settings → Email & notifications in the portal. Pass the route_id and the current revision from get_incoming_email_settings; a changed route (409) must be read again and reviewed first. This cannot create aliases or change targets. Enable, verify with a real test message, and only then change the site\'s Reply-To. Admin permission required.',
|
|
50
|
+
inputSchema: {
|
|
51
|
+
route_id: z.string().min(1),
|
|
52
|
+
revision: z.string().min(1).describe('The revision returned by get_incoming_email_settings for this route.'),
|
|
53
|
+
enabled: z.boolean(),
|
|
54
|
+
},
|
|
55
|
+
handler: withErrorBoundary(async (args, { client, siteId }) => ok(await client.put(siteId, 'delivery/inbound', {
|
|
56
|
+
route_id: args.route_id,
|
|
57
|
+
revision: args.revision,
|
|
58
|
+
enabled: args.enabled,
|
|
59
|
+
}))),
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
name: 'read_incoming_email_receipt',
|
|
63
|
+
description: 'Read one incoming email receipt (status such as forwarded, held or blocked, its reason and the forwarded delivery status) by the receipt_id shown in get_incoming_email_settings. Contains no subject, sender or body. Admin permission required.',
|
|
64
|
+
inputSchema: {
|
|
65
|
+
receipt_id: z.string().regex(/^[a-f0-9]{64}$/),
|
|
66
|
+
},
|
|
67
|
+
handler: withErrorBoundary(async (args, { client, siteId }) => ok(await client.get(siteId, `delivery/inbound/${encodeURIComponent(args.receipt_id)}`))),
|
|
68
|
+
},
|
|
69
|
+
];
|
package/dist/tools/extensions.js
CHANGED
|
@@ -66,4 +66,135 @@ export const extensionTools = [
|
|
|
66
66
|
}
|
|
67
67
|
}),
|
|
68
68
|
},
|
|
69
|
+
{
|
|
70
|
+
name: 'install_extension',
|
|
71
|
+
description: 'Install an Extension release on this site, as the portal\'s install action does. Pass the Extension id and a published release (developer organizations may also install their own draft or review releases on their own sites). granted_scopes must be a subset of the manifest\'s requested permissions — grant only what the user approved. config must satisfy manifest.config_schema; secret values are encrypted and never returned. Does not deploy: trigger_deploy afterwards to publish provisioned components. Admin permission required.',
|
|
72
|
+
inputSchema: {
|
|
73
|
+
extension_id: z.string().min(1),
|
|
74
|
+
version: z.string().min(1).describe('Release version to install.'),
|
|
75
|
+
granted_scopes: z.array(z.string()).optional().describe('Approved scopes; defaults to none.'),
|
|
76
|
+
config: z.record(z.unknown()).optional(),
|
|
77
|
+
developer_org_id: z.string().optional().describe('Organization that publishes the Extension. Defaults to the key\'s own organization.'),
|
|
78
|
+
},
|
|
79
|
+
handler: withErrorBoundary(async (args, { client, siteId }) => {
|
|
80
|
+
return ok(await client.post(siteId, 'extensions', args));
|
|
81
|
+
}),
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
name: 'set_extension_installation_status',
|
|
85
|
+
description: 'Enable or disable an installed Extension on this site. Disabling removes its components from the public snapshot and editor until re-enabled; page instances are kept. Changes apply immediately to previews; deploy to publish. Admin permission required.',
|
|
86
|
+
inputSchema: {
|
|
87
|
+
installation_id: z.string().min(1),
|
|
88
|
+
status: z.enum(['enabled', 'disabled']),
|
|
89
|
+
},
|
|
90
|
+
handler: withErrorBoundary(async (args, { client, siteId }) => {
|
|
91
|
+
return ok(await client.patch(siteId, `extensions/${encodeURIComponent(args.installation_id)}`, { status: args.status }));
|
|
92
|
+
}),
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
name: 'uninstall_extension',
|
|
96
|
+
description: 'Uninstall an Extension from this site: revokes the installation and all its server credentials and removes its block definitions. Existing page blocks remain as unavailable placeholders. Not reversible except by installing again (a new installation id). Get explicit user approval. Admin permission required.',
|
|
97
|
+
inputSchema: { installation_id: z.string().min(1) },
|
|
98
|
+
handler: withErrorBoundary(async (args, { client, siteId }) => {
|
|
99
|
+
return ok(await client.del(siteId, `extensions/${encodeURIComponent(args.installation_id)}`));
|
|
100
|
+
}),
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
name: 'pair_extension_issuer',
|
|
104
|
+
description: 'Pair this Typeroll instance\'s token issuer with the Extension provider (manifest auth.pairing_url), the portal\'s "secure connection" action. Contacts the provider. Returns the trusted issuer record. Admin permission required.',
|
|
105
|
+
inputSchema: { installation_id: z.string().min(1) },
|
|
106
|
+
handler: withErrorBoundary(async (args, { client, siteId }) => {
|
|
107
|
+
return ok(await client.post(siteId, `extensions/${encodeURIComponent(args.installation_id)}/pair`));
|
|
108
|
+
}),
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
name: 'read_extension_diagnostics',
|
|
112
|
+
description: 'Read an installation\'s diagnostics: status, health, release resolution, credential metadata (no secrets), the latest audit events and lifecycle event deliveries, and declared URL context inputs. Admin permission required.',
|
|
113
|
+
inputSchema: { installation_id: z.string().min(1) },
|
|
114
|
+
handler: withErrorBoundary(async (args, { client, siteId }) => {
|
|
115
|
+
return ok(await client.get(siteId, `extensions/${encodeURIComponent(args.installation_id)}/diagnostics`));
|
|
116
|
+
}),
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
name: 'launch_extension_admin_page',
|
|
120
|
+
description: 'Issue a single-use, short-lived launch grant for one of the Extension\'s admin pages (manifest admin.pages), as opening the page in the portal does. Returns { launch_url, method, form, expires_at }: a browser tool must POST the `form` fields to launch_url before expires_at. The provider sees the caller as api-key:{prefix}. Treat the code as a credential. For approved native pages prefer call_extension_admin. The page\'s minimum_permission applies.',
|
|
121
|
+
inputSchema: {
|
|
122
|
+
installation_id: z.string().min(1),
|
|
123
|
+
page_id: z.string().min(1).describe('Admin page id from the installation manifest.'),
|
|
124
|
+
},
|
|
125
|
+
handler: withErrorBoundary(async (args, { client, siteId }) => {
|
|
126
|
+
return ok(await client.post(siteId, `extensions/${encodeURIComponent(args.installation_id)}/launch`, { page_id: args.page_id }));
|
|
127
|
+
}),
|
|
128
|
+
},
|
|
129
|
+
];
|
|
130
|
+
const ext = (id) => `extensions/${encodeURIComponent(id)}`;
|
|
131
|
+
/** Extension developer tools — the `typeroll extension` CLI's API. They act on
|
|
132
|
+
* the key's own organization, not on a site, and need an
|
|
133
|
+
* organization-scoped key (a site key gets 403). */
|
|
134
|
+
export const developerExtensionTools = [
|
|
135
|
+
{
|
|
136
|
+
name: 'list_developer_extensions', noSite: true,
|
|
137
|
+
description: 'List Extensions registered by your organization (developer side). Requires an organization API key.',
|
|
138
|
+
handler: withErrorBoundary(async (_args, { client }) => ok(await client.developer('GET', 'extensions'))),
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
name: 'read_developer_extension', noSite: true,
|
|
142
|
+
description: 'Read one of your organization\'s Extensions with all saved releases and their status (draft, review, published, deprecated, revoked). Requires an organization API key.',
|
|
143
|
+
inputSchema: { extension_id: z.string().min(1) },
|
|
144
|
+
handler: withErrorBoundary(async (args, { client }) => ok(await client.developer('GET', ext(args.extension_id)))),
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
name: 'update_developer_extension', noSite: true,
|
|
148
|
+
description: 'Update an Extension\'s name, status (active/suspended), distribution (only while every release is a draft), trusted origins or private allow-lists. Requires an organization API key.',
|
|
149
|
+
inputSchema: {
|
|
150
|
+
extension_id: z.string().min(1),
|
|
151
|
+
name: z.string().optional(),
|
|
152
|
+
status: z.enum(['active', 'suspended']).optional(),
|
|
153
|
+
distribution: z.enum(['private', 'unlisted', 'public']).optional(),
|
|
154
|
+
trusted_origins: z.array(z.string()).optional(),
|
|
155
|
+
allowed_org_ids: z.array(z.string()).optional(),
|
|
156
|
+
allowed_site_ids: z.array(z.string()).optional(),
|
|
157
|
+
},
|
|
158
|
+
handler: withErrorBoundary(async (args, { client }) => {
|
|
159
|
+
const { extension_id, ...patch } = args;
|
|
160
|
+
return ok(await client.developer('PATCH', ext(extension_id), patch));
|
|
161
|
+
}),
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
name: 'save_extension_version', noSite: true,
|
|
165
|
+
description: 'Save (create or replace) a draft release from a full Extension manifest. Published releases are immutable; bump manifest.version instead. Validate locally with `typeroll extension validate` first. Requires an organization API key.',
|
|
166
|
+
inputSchema: {
|
|
167
|
+
extension_id: z.string().min(1),
|
|
168
|
+
manifest: z.record(z.unknown()).describe('Complete manifest; its id must equal extension_id.'),
|
|
169
|
+
},
|
|
170
|
+
handler: withErrorBoundary(async (args, { client }) => ok(await client.developer('POST', `${ext(args.extension_id)}/versions`, { manifest: args.manifest }))),
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
name: 'publish_extension_version', noSite: true,
|
|
174
|
+
description: 'Publish a saved release after verifying its asset digests. Private releases become installable for allowed organizations and sites; public releases enter review before catalog discovery. Installations follow compatible published releases automatically, so get user approval. Requires an organization API key.',
|
|
175
|
+
inputSchema: { extension_id: z.string().min(1), version: z.string().min(1) },
|
|
176
|
+
handler: withErrorBoundary(async (args, { client }) => ok(await client.developer('POST', `${ext(args.extension_id)}/versions/${encodeURIComponent(args.version)}/publish`))),
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
name: 'set_extension_version_lifecycle', noSite: true,
|
|
180
|
+
description: 'Deprecate or revoke a release. Revoking stops installations from resolving to it. Requires an organization API key.',
|
|
181
|
+
inputSchema: {
|
|
182
|
+
extension_id: z.string().min(1),
|
|
183
|
+
version: z.string().min(1),
|
|
184
|
+
status: z.enum(['deprecated', 'revoked']),
|
|
185
|
+
reason: z.string().optional(),
|
|
186
|
+
},
|
|
187
|
+
handler: withErrorBoundary(async (args, { client }) => {
|
|
188
|
+
const { extension_id, version, ...body } = args;
|
|
189
|
+
return ok(await client.developer('PATCH', `${ext(extension_id)}/versions/${encodeURIComponent(version)}`, body));
|
|
190
|
+
}),
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
name: 'list_developer_extension_installations', noSite: true,
|
|
194
|
+
description: 'List operational metadata (status, versions, health; never config) for your Extension\'s installations on one customer site. Requires an organization API key and the customer\'s organization and site ids.',
|
|
195
|
+
inputSchema: { extension_id: z.string().min(1), owner_org_id: z.string().min(1), site_id: z.string().min(1) },
|
|
196
|
+
handler: withErrorBoundary(async (args, { client }) => ok(await client.developer('GET', `${ext(args.extension_id)}/installations`, undefined, {
|
|
197
|
+
owner_org_id: args.owner_org_id, site_id: args.site_id,
|
|
198
|
+
}))),
|
|
199
|
+
},
|
|
69
200
|
];
|
package/dist/tools/forms.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
// Forms — agents can create/update/delete forms,
|
|
1
|
+
// Forms — agents can create/update/delete forms, manage their email
|
|
2
|
+
// notifications and webhooks, and read submissions, like the portal.
|
|
2
3
|
// The customer-facing submit endpoint is HMAC-signed and lives at
|
|
3
4
|
// /api/forms/submit. Agents place forms by reference: core/form in block mode,
|
|
4
5
|
// or <x-form id="…" /> in HTML mode. Preview/build expands either reference
|
|
@@ -33,6 +34,16 @@ const targetSchema = z.object({
|
|
|
33
34
|
context_params: z.array(z.string()).max(8).optional().describe('Explicit URL query keys forwarded to the app hydration endpoint; reserved security keys are rejected.'),
|
|
34
35
|
installation_id: z.string(), path: z.string(), hydrate: z.boolean().optional(), session_param: z.string().optional(),
|
|
35
36
|
}).optional().describe('Admin-only: bind to a declared POST route of an enabled app installation. Read its authenticated guide first.');
|
|
37
|
+
// Actions run after a successful submission: email notifications, signed
|
|
38
|
+
// webhooks and app-provided action types. Same validation as the portal's
|
|
39
|
+
// Forms editor; admin permission required, as in the portal.
|
|
40
|
+
const actionSchema = z.object({
|
|
41
|
+
id: z.string().optional().describe('Keep an existing action\'s id to update it in place (webhooks keep their stored secret).'),
|
|
42
|
+
type: z.string().describe('"email", "webhook", or an action type provided by an installed app (see get_form_capabilities).'),
|
|
43
|
+
config: z.record(z.string(), z.unknown()).describe('email: { to, subject, body, cc?, bcc?, reply_to?, include_all?, format?: "html"|"text" } — to/subject/body may use {{field}} placeholders, e.g. to: "{{email}}" for a confirmation to the visitor. ' +
|
|
44
|
+
'webhook: { url (https), fields: ["name","email"] (only these values are sent), secret (signing secret; send "••••••••" to keep the stored one) }.'),
|
|
45
|
+
});
|
|
46
|
+
const actionsDescription = 'The complete list of actions after a submission (replaces the current list): email notifications, webhooks, app actions (get_form_capabilities lists the types and their config). Read the form first and send back the actions you keep. Emails send through the provider set with set_email_settings. Requires admin permission, like the portal.';
|
|
36
47
|
export const formTools = [
|
|
37
48
|
{
|
|
38
49
|
name: 'list_forms',
|
|
@@ -42,9 +53,14 @@ export const formTools = [
|
|
|
42
53
|
return ok(res);
|
|
43
54
|
}),
|
|
44
55
|
},
|
|
56
|
+
{
|
|
57
|
+
name: 'get_form_capabilities',
|
|
58
|
+
description: 'List the action types a form can run after a submission (core "email" and "webhook" plus types provided by installed apps) and the prefill sources, each with its config field schema — the same choices as the portal\'s Forms editor. Read this before writing `actions` with create_form/update_form. Admin permission required.',
|
|
59
|
+
handler: withErrorBoundary(async (_args, { client, siteId }) => ok(await client.get(siteId, 'form-capabilities'))),
|
|
60
|
+
},
|
|
45
61
|
{
|
|
46
62
|
name: 'read_form',
|
|
47
|
-
description: 'Read one form by id, including fields/steps, submit_text, and
|
|
63
|
+
description: 'Read one form by id, including fields/steps, submit_text, success_message and, for admins, its actions (email notifications and webhooks; secrets masked). Place it with a `core/form` block using data.form_id on a block-mode page, or `<x-form id="…" />` in HTML mode. Both references are expanded server-side with validation, signed token, initial state, and the shared runtime.',
|
|
48
64
|
inputSchema: { form_id: z.string() },
|
|
49
65
|
handler: withErrorBoundary(async (args, { client, siteId }) => {
|
|
50
66
|
const res = await client.get(siteId, `forms/${encodeURIComponent(args.form_id)}`);
|
|
@@ -62,6 +78,7 @@ export const formTools = [
|
|
|
62
78
|
submit_text: z.string().optional(),
|
|
63
79
|
success_message: z.string().optional().describe('Shown after the final step. May contain basic HTML (links, emphasis); it is sanitized.'),
|
|
64
80
|
success_redirect_url: z.string().optional().describe('After the final step, send the visitor to this absolute http(s) URL or root-relative path (for example a booking or thank-you page) instead of showing success_message. Pass an empty string to clear.'),
|
|
81
|
+
actions: z.array(actionSchema).optional().describe(actionsDescription),
|
|
65
82
|
target: targetSchema,
|
|
66
83
|
},
|
|
67
84
|
handler: withErrorBoundary(async (args, { client, siteId }) => {
|
|
@@ -74,7 +91,7 @@ export const formTools = [
|
|
|
74
91
|
},
|
|
75
92
|
{
|
|
76
93
|
name: 'update_form',
|
|
77
|
-
description: 'Patch a form. Provide only what you want to change. Passing `steps` replaces the whole step list; passing `fields` (sugar for simple forms) replaces the whole step list with ONE static step built from them. The existing form_id is immutable.',
|
|
94
|
+
description: 'Patch a form. Provide only what you want to change. Passing `steps` replaces the whole step list; passing `fields` (sugar for simple forms) replaces the whole step list with ONE static step built from them. Passing `actions` replaces the email notifications and webhooks (admin). The existing form_id is immutable.',
|
|
78
95
|
inputSchema: {
|
|
79
96
|
form_id: z.string(),
|
|
80
97
|
patch: z.object({
|
|
@@ -84,6 +101,7 @@ export const formTools = [
|
|
|
84
101
|
submit_text: z.string().optional(),
|
|
85
102
|
success_message: z.string().optional().describe('Shown after the final step. May contain basic HTML (links, emphasis); it is sanitized.'),
|
|
86
103
|
success_redirect_url: z.string().optional().describe('After the final step, send the visitor to this absolute http(s) URL or root-relative path (for example a booking or thank-you page) instead of showing success_message. Pass an empty string to clear.'),
|
|
104
|
+
actions: z.array(actionSchema).optional().describe(actionsDescription),
|
|
87
105
|
target: targetSchema,
|
|
88
106
|
}),
|
|
89
107
|
},
|
|
@@ -106,7 +124,7 @@ export const formTools = [
|
|
|
106
124
|
},
|
|
107
125
|
{
|
|
108
126
|
name: 'list_form_submissions',
|
|
109
|
-
description: 'List submissions received for a form, newest first, cursor-paginated (cap 200 per page). Use this to help a customer triage inbound contact / lead submissions.',
|
|
127
|
+
description: 'List submissions received for a form, newest first, cursor-paginated (cap 200 per page). Admins also see each submission\'s webhook delivery status. Use this to help a customer triage inbound contact / lead submissions; read_form_submission reads one.',
|
|
110
128
|
inputSchema: {
|
|
111
129
|
form_id: z.string(),
|
|
112
130
|
limit: z.number().int().min(1).max(200).optional(),
|
|
@@ -118,6 +136,18 @@ export const formTools = [
|
|
|
118
136
|
return ok(res);
|
|
119
137
|
}),
|
|
120
138
|
},
|
|
139
|
+
{
|
|
140
|
+
name: 'read_form_submission',
|
|
141
|
+
description: 'Read one submission of a form by id (from list_form_submissions): the submitted values, status (partial or complete), step and timestamps. Admins also see its webhook delivery status, as in the portal.',
|
|
142
|
+
inputSchema: {
|
|
143
|
+
form_id: z.string(),
|
|
144
|
+
submission_id: z.string(),
|
|
145
|
+
},
|
|
146
|
+
handler: withErrorBoundary(async (args, { client, siteId }) => {
|
|
147
|
+
const res = await client.get(siteId, `forms/${encodeURIComponent(args.form_id)}/submissions/${encodeURIComponent(args.submission_id)}`);
|
|
148
|
+
return ok(res);
|
|
149
|
+
}),
|
|
150
|
+
},
|
|
121
151
|
{
|
|
122
152
|
name: 'delete_form_submission',
|
|
123
153
|
description: 'Delete a single submission from a form\'s inbox. Use this to remove individual entries (test submissions, spam) without touching the rest — delete_form with delete_submissions is the only way to bulk-delete. Get submission ids from list_form_submissions.',
|
package/dist/tools/helpers.js
CHANGED
|
@@ -53,6 +53,13 @@ export function withErrorBoundary(handler) {
|
|
|
53
53
|
};
|
|
54
54
|
}
|
|
55
55
|
/** Shared zod fragment for the optional `version` parameter. */
|
|
56
|
+
/**
|
|
57
|
+
* MCP writes Page field values with the same authority as an editor in the
|
|
58
|
+
* portal. Only a value the listed business set itself needs a stated reason.
|
|
59
|
+
*/
|
|
60
|
+
export const writeAuthority = {
|
|
61
|
+
override_reason: z.string().max(500).optional().describe('Why a value the listed business set itself (through its edit link) is being replaced. Required only for those values, as in the portal; the write otherwise returns 409 naming the field.'),
|
|
62
|
+
};
|
|
56
63
|
export const versionParam = z
|
|
57
64
|
.string()
|
|
58
65
|
.optional()
|
package/dist/tools/media.js
CHANGED
|
@@ -18,6 +18,12 @@ export const mediaTools = [
|
|
|
18
18
|
inputSchema: {},
|
|
19
19
|
handler: withErrorBoundary(async (_args, { client, siteId }) => ok(await client.get(siteId, 'media/import'))),
|
|
20
20
|
},
|
|
21
|
+
{
|
|
22
|
+
name: 'get_media_upload_status',
|
|
23
|
+
description: 'Pre-flight for uploads: whether media uploads can go through for this site right now (organization storage connected, or platform storage configured), with the reason and a settings_url when they cannot. Same check as the banner in the portal media library. Requires site read access.',
|
|
24
|
+
inputSchema: {},
|
|
25
|
+
handler: withErrorBoundary(async (_args, { client, siteId }) => ok(await client.get(siteId, 'media/upload-status'))),
|
|
26
|
+
},
|
|
21
27
|
{
|
|
22
28
|
name: 'read_private_media_url',
|
|
23
29
|
description: 'Get a 60-second read URL for a private media original. Use it for authorized image inspection; never store it in page content or generated source. Use the stable media URL or ID in content.',
|
|
@@ -229,6 +235,12 @@ export const mediaTools = [
|
|
|
229
235
|
return ok(res);
|
|
230
236
|
}),
|
|
231
237
|
},
|
|
238
|
+
{
|
|
239
|
+
name: 'purge_site_media',
|
|
240
|
+
description: 'Permanently delete every media object and record of an ARCHIVED site (archive_site first; an active site returns 409). Irreversible — the step before retiring a site for good, same as the portal action. Requires an admin key of the organization that owns the site; shared access is refused. Returns counts; HTTP 207 with `failed` lists objects that could not be removed (their records are kept) — call again to retry exactly those. Confirm with the user before calling.',
|
|
241
|
+
inputSchema: {},
|
|
242
|
+
handler: withErrorBoundary(async (_args, { client, siteId }) => ok(await client.post(siteId, 'media/purge'))),
|
|
243
|
+
},
|
|
232
244
|
{
|
|
233
245
|
name: 'delete_media',
|
|
234
246
|
description: 'Delete a media item\'s metadata. (R2 object cleanup is a separate ops task.)',
|