@typeroll/mcp-server 0.7.12 → 0.10.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/AGENTS.md +34 -8
- package/README.md +8 -3
- package/dist/index.js +0 -0
- package/dist/server.js +2 -0
- package/dist/tools/collections.js +8 -0
- package/dist/tools/domain.js +98 -0
- package/dist/tools/media.js +47 -2
- package/dist/tools/pages.js +16 -0
- package/dist/tools/settings.js +2 -1
- package/package.json +1 -1
package/AGENTS.md
CHANGED
|
@@ -287,24 +287,50 @@ first if you're unsure what's writable.
|
|
|
287
287
|
```
|
|
288
288
|
# Image lives on a URL somewhere (Unsplash, customer's existing CDN):
|
|
289
289
|
upload_media_from_url source_url="https://..." alt_text="Hero photo of …"
|
|
290
|
-
→ returns { media_id, cdn_url, … }
|
|
290
|
+
→ returns { media_id, cdn_url, finalize: {…}, finalize_error: null }
|
|
291
291
|
|
|
292
292
|
# OR image lives in your memory (image-gen output):
|
|
293
293
|
upload_media_inline filename="hero.png" content_type="image/png"
|
|
294
294
|
data_base64="iVBORw0KGgo…"
|
|
295
295
|
→ returns the same shape
|
|
296
296
|
|
|
297
|
-
#
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
297
|
+
# Both tools auto-finalize after PUT: immutable Cache-Control on the
|
|
298
|
+
# original PLUS AVIF/WebP variants at 320/640/1024/1920. No manual
|
|
299
|
+
# generate_image_variants call needed. The site-template renderer reads
|
|
300
|
+
# the variants array off the Media doc and emits <picture> automatically
|
|
301
|
+
# — you can keep the <img src="{cdn_url}"> markup simple.
|
|
301
302
|
|
|
302
303
|
# Then embed in a page:
|
|
303
304
|
read_page page_id=...
|
|
304
|
-
|
|
305
|
-
update_page page_id=... patch={ html_content: "<...><img src='{cdn_url}' .../>" }
|
|
305
|
+
update_page page_id=... patch={ html_content: "<...><img src='{cdn_url}' alt='…' /></...>" }
|
|
306
306
|
```
|
|
307
307
|
|
|
308
|
+
### "Stop an image over-fetching a too-large variant"
|
|
309
|
+
|
|
310
|
+
When an image renders much narrower than the viewport (a container-constrained
|
|
311
|
+
hero, a sidebar thumbnail), the default `<picture sizes>` of
|
|
312
|
+
`(max-width: 768px) 100vw, 800px` makes the browser pull a wider srcset variant
|
|
313
|
+
than it needs — Lighthouse flags it as wasted bytes. Three levers, narrowest
|
|
314
|
+
wins:
|
|
315
|
+
|
|
316
|
+
```
|
|
317
|
+
# 1. Per-image: put a real `sizes` on the <img>. Survives the transform verbatim.
|
|
318
|
+
update_page page_id=... patch={ html_content:
|
|
319
|
+
"<img src='{cdn_url}' alt='…' sizes='(max-width: 640px) 360px, 560px' />" }
|
|
320
|
+
|
|
321
|
+
# 2. Per-page default (applies to every image on the page that has no own sizes):
|
|
322
|
+
update_page page_id=... patch={ image_sizes_default: "(max-width: 640px) 360px, 560px" }
|
|
323
|
+
|
|
324
|
+
# 3. Site-wide default (fallback under the page default):
|
|
325
|
+
update_site_settings image_sizes_default="(max-width: 640px) 360px, 560px"
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
Precedence: per-image `sizes` > page `image_sizes_default` >
|
|
329
|
+
site `image_sizes_default` > the generic built-in. To opt a single image out of
|
|
330
|
+
the platform's auto-`<picture>` entirely, hand-write your own `<picture>` with
|
|
331
|
+
custom `<source media=…>` — the transform leaves an existing `<picture>`
|
|
332
|
+
untouched (it no longer re-wraps the inner `<img>`).
|
|
333
|
+
|
|
308
334
|
### "Fill missing alt-text across the media library"
|
|
309
335
|
|
|
310
336
|
```
|
|
@@ -438,7 +464,7 @@ stakeholder review.
|
|
|
438
464
|
| **Global blocks (partials)** | `list_partials` (summary by default), `read_partial`, `create_free_block`, `update_partial`, `replace_partial`, `delete_partial`, `find_pages_using_block`, `list_blocks_with_usage` |
|
|
439
465
|
| **Block types** | `list_block_types`, `read_block_type`, `find_pages_using_block_type`, `export_block_types`, `import_block_types` |
|
|
440
466
|
| **Collections** | `create_collection`, `update_collection_schema`, `delete_collection`, `list_collections`, `read_collection`, `list_collection_items` (richtext hidden by default), `read_collection_item`, `batch_read_collection_items`, `create_collection_item`, `update_collection_item`, `delete_collection_item`, `regenerate_collection_listing` |
|
|
441
|
-
| **Media** | `list_media`, `read_media`, `create_upload_url`, `upload_media_from_url`, `upload_media_inline`, `update_media`, `delete_media`, `generate_image_variants`, `suggest_alt_text_context` |
|
|
467
|
+
| **Media** | `list_media`, `read_media`, `create_upload_url`, `upload_media_from_url`, `upload_media_inline`, `update_media`, `delete_media`, `finalize_media`, `finalize_all_media`, `generate_image_variants`, `suggest_alt_text_context` |
|
|
442
468
|
| **Redirects** | `list_redirects`, `create_redirect`, `delete_redirect` |
|
|
443
469
|
| **Forms** | `list_forms`, `read_form`, `create_form`, `update_form`, `delete_form`, `list_form_submissions` |
|
|
444
470
|
| **Settings** | `update_site_settings` (whitelist) |
|
package/README.md
CHANGED
|
@@ -105,9 +105,14 @@ the full reference + concrete operation recipes.
|
|
|
105
105
|
itself (incl. `route_template` for per-item URLs); list/read/batch-
|
|
106
106
|
read/create/update/delete items.
|
|
107
107
|
- **Media** — list, read, signed upload URLs, `upload_media_from_url`,
|
|
108
|
-
`upload_media_inline
|
|
109
|
-
|
|
110
|
-
|
|
108
|
+
`upload_media_inline` (both auto-finalize after PUT — see below),
|
|
109
|
+
patch metadata, delete, `finalize_media` (per-item: applies immutable
|
|
110
|
+
Cache-Control + generates AVIF/WebP srcset variants — call after
|
|
111
|
+
`create_upload_url`'s raw PUT path), `finalize_all_media` (bulk
|
|
112
|
+
backfill for legacy libraries), `generate_image_variants` (the
|
|
113
|
+
variant half of finalize, kept for surgical reruns),
|
|
114
|
+
`suggest_alt_text_context` (returns a tuned prompt for your own
|
|
115
|
+
vision model).
|
|
111
116
|
- **Redirects** — list, create, delete. Plus automatic 301 on slug change.
|
|
112
117
|
- **Forms** — list, read, create, update, delete, list submissions.
|
|
113
118
|
- **Settings** — read + patch (scripts and custom CSS not exposed).
|
package/dist/index.js
CHANGED
|
File without changes
|
package/dist/server.js
CHANGED
|
@@ -24,6 +24,7 @@ import { blockTypeTools } from './tools/block-types.js';
|
|
|
24
24
|
import { pageBlockTools } from './tools/page-blocks.js';
|
|
25
25
|
import { settingsTools } from './tools/settings.js';
|
|
26
26
|
import { siteTools } from './tools/sites.js';
|
|
27
|
+
import { domainTools } from './tools/domain.js';
|
|
27
28
|
import { fail } from './tools/helpers.js';
|
|
28
29
|
const PERM_RANK = { read: 0, write: 1, admin: 2 };
|
|
29
30
|
/**
|
|
@@ -73,6 +74,7 @@ export function buildServer(options) {
|
|
|
73
74
|
...versionTools,
|
|
74
75
|
...deployTools,
|
|
75
76
|
...previewTools,
|
|
77
|
+
...domainTools,
|
|
76
78
|
];
|
|
77
79
|
// SDK's registerTool has deeply-nested generics we can't unify across a
|
|
78
80
|
// heterogeneous tool list at compile time — same shrug as stdio's index.ts.
|
|
@@ -52,6 +52,14 @@ export const collectionTools = [
|
|
|
52
52
|
sort_dir: z.enum(['asc', 'desc']).optional(),
|
|
53
53
|
route_template: z.string().optional(),
|
|
54
54
|
item_template_html: z.string().optional(),
|
|
55
|
+
schema_type: z
|
|
56
|
+
.string()
|
|
57
|
+
.optional()
|
|
58
|
+
.describe('Free-form Schema.org type for auto JSON-LD on every item route ("BlogPosting", "PodcastEpisode", "Product", "Event", "Recipe", "Course", …). Pair with schema_field_map when item field names differ from the schema property names.'),
|
|
59
|
+
schema_field_map: z
|
|
60
|
+
.record(z.string())
|
|
61
|
+
.optional()
|
|
62
|
+
.describe('Override the default field→schema-property mapping. Example: { "audio_url": "contentUrl", "show_notes": "description" }. Values use camelCase (Schema.org convention).'),
|
|
55
63
|
})
|
|
56
64
|
.passthrough(),
|
|
57
65
|
version: versionParam,
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// Custom-domain lifecycle tools. Wrap the four /api/v1/sites/{id}/domain/*
|
|
2
|
+
// routes so an agent can read, attach, poll, activate, or remove a
|
|
3
|
+
// custom domain through MCP. Admin-only on the server side; site-scoped
|
|
4
|
+
// keys are implicitly admin, org-scoped keys forward the share's
|
|
5
|
+
// permission.
|
|
6
|
+
//
|
|
7
|
+
// State machine recap (see docs/domain-lifecycle-plan.md):
|
|
8
|
+
//
|
|
9
|
+
// no domain ──add_domain──▶ pending
|
|
10
|
+
// pending ──poll_domain──▶ pending | verified | failed
|
|
11
|
+
// verified ──activate_domain──▶ live
|
|
12
|
+
// any state ──remove_domain──▶ no domain
|
|
13
|
+
//
|
|
14
|
+
// `production_url` on the site response (list_sites / read_site) only
|
|
15
|
+
// reflects the custom domain when status === 'live'. Until then,
|
|
16
|
+
// agents should send users to the fallback URL.
|
|
17
|
+
import { z } from 'zod';
|
|
18
|
+
import { ok, withErrorBoundary } from './helpers.js';
|
|
19
|
+
export const domainTools = [
|
|
20
|
+
{
|
|
21
|
+
name: 'read_domain',
|
|
22
|
+
description: "Read the current state of the site's custom domain. Returns " +
|
|
23
|
+
'`{ hostname, status, dns_target, verified_at, failure_reason }` ' +
|
|
24
|
+
"or `null` when no domain is attached. Status is one of 'pending' " +
|
|
25
|
+
"(DNS not yet verified), 'verified' (DNS + cert OK, awaiting customer " +
|
|
26
|
+
"activation), 'live' (production URL is now the custom domain), " +
|
|
27
|
+
"'failed' (Cloudflare returned an error — see failure_reason).",
|
|
28
|
+
inputSchema: {},
|
|
29
|
+
handler: withErrorBoundary(async (_args, { client, siteId }) => {
|
|
30
|
+
const res = await client.get(siteId, 'domain');
|
|
31
|
+
return ok(res);
|
|
32
|
+
}),
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
name: 'add_domain',
|
|
36
|
+
description: "Attach a custom domain to this site. The site MUST NOT already have " +
|
|
37
|
+
"a domain set — remove the existing one first. Returns the new state, " +
|
|
38
|
+
"including the CNAME target the customer must point DNS at. Status " +
|
|
39
|
+
"starts as 'pending'; once DNS is correct + cert is issued, " +
|
|
40
|
+
"`poll_domain` will flip it to 'verified', then `activate_domain` " +
|
|
41
|
+
"makes it production.",
|
|
42
|
+
inputSchema: {
|
|
43
|
+
hostname: z
|
|
44
|
+
.string()
|
|
45
|
+
.describe('Hostname to attach, e.g. "www.example.com". Leading https:// and trailing slashes are stripped.'),
|
|
46
|
+
},
|
|
47
|
+
handler: withErrorBoundary(async (args, { client, siteId }) => {
|
|
48
|
+
const res = await client.post(siteId, 'domain', { hostname: args.hostname });
|
|
49
|
+
return ok(res);
|
|
50
|
+
}),
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
name: 'poll_domain',
|
|
54
|
+
description: "Ask Cloudflare for the current state of the attached domain and " +
|
|
55
|
+
"update the site doc accordingly. Idempotent — call after DNS " +
|
|
56
|
+
"changes propagate. Returns the new state; status may have advanced " +
|
|
57
|
+
"from 'pending' to 'verified' or 'failed'.",
|
|
58
|
+
inputSchema: {},
|
|
59
|
+
handler: withErrorBoundary(async (_args, { client, siteId }) => {
|
|
60
|
+
const res = await client.post(siteId, 'domain/poll');
|
|
61
|
+
return ok(res);
|
|
62
|
+
}),
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
name: 'activate_domain',
|
|
66
|
+
description: "Make the verified domain the production URL. Precondition: status " +
|
|
67
|
+
"must be 'verified' (or already 'live' — idempotent). After this " +
|
|
68
|
+
"call, the site's `production_url` switches from the fallback URL " +
|
|
69
|
+
"to https://<hostname>. " +
|
|
70
|
+
"By default also enqueues a production deploy so the canonical URL, " +
|
|
71
|
+
"sitemap, and JSON-LD update to reflect the new hostname — the " +
|
|
72
|
+
"response includes `deploy.job_id` you can poll. Pass " +
|
|
73
|
+
"`auto_deploy: false` to skip the deploy (the canonical stays at " +
|
|
74
|
+
"the previous URL until the customer deploys manually).",
|
|
75
|
+
inputSchema: {
|
|
76
|
+
auto_deploy: z
|
|
77
|
+
.boolean()
|
|
78
|
+
.optional()
|
|
79
|
+
.describe('Default true. Enqueue a production deploy after activation so canonical URLs update.'),
|
|
80
|
+
},
|
|
81
|
+
handler: withErrorBoundary(async (args, { client, siteId }) => {
|
|
82
|
+
const res = await client.post(siteId, 'domain/activate', args);
|
|
83
|
+
return ok(res);
|
|
84
|
+
}),
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
name: 'remove_domain',
|
|
88
|
+
description: "Detach the custom domain from this site. Removes it from Cloudflare " +
|
|
89
|
+
"Pages and clears the site doc. The fallback subdomain remains. " +
|
|
90
|
+
"Use this when the customer wants to start over with a different " +
|
|
91
|
+
"hostname or move the site to a different domain provider.",
|
|
92
|
+
inputSchema: {},
|
|
93
|
+
handler: withErrorBoundary(async (_args, { client, siteId }) => {
|
|
94
|
+
const res = await client.del(siteId, 'domain');
|
|
95
|
+
return ok(res);
|
|
96
|
+
}),
|
|
97
|
+
},
|
|
98
|
+
];
|
package/dist/tools/media.js
CHANGED
|
@@ -59,7 +59,7 @@ export const mediaTools = [
|
|
|
59
59
|
},
|
|
60
60
|
{
|
|
61
61
|
name: 'create_upload_url',
|
|
62
|
-
description: 'Mint a 5-min signed PUT URL for direct-to-R2 upload. After the upload completes, the CDN url is what you reference in <img src="…">.
|
|
62
|
+
description: 'Mint a 5-min signed PUT URL for direct-to-R2 upload. After the upload completes, the CDN url is what you reference in <img src="…">. REQUIRED follow-up: call `finalize_media` after the PUT succeeds — without it the original ships with no Cache-Control (Lighthouse will flag it) and no responsive variants get generated. The response carries `finalize_url` as a reminder. Prefer `upload_media_from_url` or `upload_media_inline` if you have the bytes in hand — they do the PUT + finalize for you in one call.',
|
|
63
63
|
inputSchema: {
|
|
64
64
|
filename: z.string().min(1),
|
|
65
65
|
content_type: z.string().min(1).describe('e.g. "image/png", "image/jpeg", "application/pdf"'),
|
|
@@ -108,12 +108,27 @@ export const mediaTools = [
|
|
|
108
108
|
if (!putRes.ok) {
|
|
109
109
|
throw new Error(`R2 upload failed: ${putRes.status} ${putRes.statusText}`);
|
|
110
110
|
}
|
|
111
|
+
// 4. Auto-finalize: set immutable Cache-Control on the original and
|
|
112
|
+
// generate AVIF/WebP responsive variants. Best-effort — if it
|
|
113
|
+
// fails (variant pipeline 500, missing env var on the server),
|
|
114
|
+
// the upload itself still succeeded so we return ok with a
|
|
115
|
+
// `finalize_error` flag instead of throwing.
|
|
116
|
+
let finalizeResult = null;
|
|
117
|
+
let finalizeError = null;
|
|
118
|
+
try {
|
|
119
|
+
finalizeResult = await client.post(siteId, `media/${encodeURIComponent(mint.media_id)}/finalize`);
|
|
120
|
+
}
|
|
121
|
+
catch (e) {
|
|
122
|
+
finalizeError = e instanceof Error ? e.message : String(e);
|
|
123
|
+
}
|
|
111
124
|
return ok({
|
|
112
125
|
media_id: mint.media_id,
|
|
113
126
|
cdn_url: mint.cdn_url,
|
|
114
127
|
filename,
|
|
115
128
|
content_type: contentType,
|
|
116
129
|
size_bytes: buf.byteLength,
|
|
130
|
+
finalize: finalizeResult,
|
|
131
|
+
finalize_error: finalizeError,
|
|
117
132
|
});
|
|
118
133
|
}),
|
|
119
134
|
},
|
|
@@ -149,12 +164,24 @@ export const mediaTools = [
|
|
|
149
164
|
if (!putRes.ok) {
|
|
150
165
|
throw new Error(`R2 upload failed: ${putRes.status} ${putRes.statusText}`);
|
|
151
166
|
}
|
|
167
|
+
// Auto-finalize (cache headers + AVIF/WebP variants). See
|
|
168
|
+
// upload_media_from_url for the same pattern + rationale.
|
|
169
|
+
let finalizeResult = null;
|
|
170
|
+
let finalizeError = null;
|
|
171
|
+
try {
|
|
172
|
+
finalizeResult = await client.post(siteId, `media/${encodeURIComponent(mint.media_id)}/finalize`);
|
|
173
|
+
}
|
|
174
|
+
catch (e) {
|
|
175
|
+
finalizeError = e instanceof Error ? e.message : String(e);
|
|
176
|
+
}
|
|
152
177
|
return ok({
|
|
153
178
|
media_id: mint.media_id,
|
|
154
179
|
cdn_url: mint.cdn_url,
|
|
155
180
|
filename: args.filename,
|
|
156
181
|
content_type: args.content_type,
|
|
157
182
|
size_bytes: buf.byteLength,
|
|
183
|
+
finalize: finalizeResult,
|
|
184
|
+
finalize_error: finalizeError,
|
|
158
185
|
});
|
|
159
186
|
}),
|
|
160
187
|
},
|
|
@@ -174,13 +201,31 @@ export const mediaTools = [
|
|
|
174
201
|
},
|
|
175
202
|
{
|
|
176
203
|
name: 'generate_image_variants',
|
|
177
|
-
description: 'Run the build-time srcset pipeline for one image: produces webp + avif variants at 320 / 640 / 1024 / 1920 (skipping upscales), uploads them to R2 alongside the original, and patches the media doc with a variants[] array. Synchronous; takes ~1-5s per image. Skips PDFs and non-image media without erroring.
|
|
204
|
+
description: 'Run the build-time srcset pipeline for one image: produces webp + avif variants at 320 / 640 / 1024 / 1920 (skipping upscales), uploads them to R2 alongside the original, and patches the media doc with a variants[] array. Synchronous; takes ~1-5s per image. Skips PDFs and non-image media without erroring. NOTE: prefer `finalize_media` for new uploads — it does this PLUS sets immutable Cache-Control on the original. Use this tool only for surgical variant reruns.',
|
|
178
205
|
inputSchema: { media_id: z.string() },
|
|
179
206
|
handler: withErrorBoundary(async (args, { client, siteId }) => {
|
|
180
207
|
const res = await client.post(siteId, `media/${encodeURIComponent(args.media_id)}/generate-variants`);
|
|
181
208
|
return ok(res);
|
|
182
209
|
}),
|
|
183
210
|
},
|
|
211
|
+
{
|
|
212
|
+
name: 'finalize_media',
|
|
213
|
+
description: 'Post-upload finalize for a single image: (1) applies immutable Cache-Control (public, max-age=31536000) to the R2 original via in-place CopyObject so the CDN caches it for a year, and (2) generates AVIF/WebP responsive variants if not already present. Call this immediately after a successful upload (the presigned PUT does NOT set cache headers — without finalize, Cloudflare defaults to ~4h and every visit re-fetches the original). Idempotent: safe to call twice. Skips variant work when variants already exist on the doc.',
|
|
214
|
+
inputSchema: { media_id: z.string() },
|
|
215
|
+
handler: withErrorBoundary(async (args, { client, siteId }) => {
|
|
216
|
+
const res = await client.post(siteId, `media/${encodeURIComponent(args.media_id)}/finalize`);
|
|
217
|
+
return ok(res);
|
|
218
|
+
}),
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
name: 'finalize_all_media',
|
|
222
|
+
description: 'Backfill version of finalize_media: walks the entire media library, applies cache headers + variant generation to every item missing them. Use this once on legacy sites to fix Lighthouse "cache lifetimes" + "image delivery" warnings in one go. Synchronous, can take minutes on large libraries (~1-5s per image needing variants, ~50ms for cache-only). Idempotent.',
|
|
223
|
+
inputSchema: {},
|
|
224
|
+
handler: withErrorBoundary(async (_args, { client, siteId }) => {
|
|
225
|
+
const res = await client.post(siteId, 'media/finalize-all');
|
|
226
|
+
return ok(res);
|
|
227
|
+
}),
|
|
228
|
+
},
|
|
184
229
|
{
|
|
185
230
|
name: 'suggest_alt_text_context',
|
|
186
231
|
description: 'Get everything you need to generate good alt-text for a media item: the image URL, a tuned SEO/accessibility prompt, the site\'s content language, and the list of pages where the image is used (with the nearest heading per use site). YOU run the actual vision call with your own model — this endpoint does NOT generate text. After you decide on alt-text, save it with update_media.',
|
package/dist/tools/pages.js
CHANGED
|
@@ -84,10 +84,13 @@ export const pageTools = [
|
|
|
84
84
|
status: z.enum(['draft', 'review', 'unlisted', 'published']).optional(),
|
|
85
85
|
seo_title: z.string().optional(),
|
|
86
86
|
seo_description: z.string().optional(),
|
|
87
|
+
seo_image_alt: z.string().optional(),
|
|
88
|
+
schema_type: z.string().optional().describe('Free-form Schema.org type ("Service", "Course", "Product", …) for auto JSON-LD.'),
|
|
87
89
|
kind: z.enum(['page', 'article']).optional(),
|
|
88
90
|
author: z.string().optional(),
|
|
89
91
|
language: z.string().optional().describe('BCP-47 tag overriding the site default (e.g. "en" on an otherwise Swedish site).'),
|
|
90
92
|
template: z.string().optional().describe('Page template id (PageTemplate). Wraps the body in the template tree at render time.'),
|
|
93
|
+
image_sizes_default: z.string().optional().describe('Per-page default `sizes` for responsive images (e.g. "(max-width: 640px) 360px, 560px"). Overrides the site setting; a per-<img> `sizes` attr still wins. Set when this page\'s images render narrower than the generic default so the browser stops over-fetching.'),
|
|
91
94
|
version: versionParam,
|
|
92
95
|
},
|
|
93
96
|
handler: withErrorBoundary(async (args, { client, siteId }) => {
|
|
@@ -115,9 +118,22 @@ export const pageTools = [
|
|
|
115
118
|
seo_title: z.string().optional(),
|
|
116
119
|
seo_description: z.string().optional(),
|
|
117
120
|
og_image: z.string().optional(),
|
|
121
|
+
seo_image_alt: z.string().optional().describe('Alt text for og:image/twitter:image. Falls back to first <img alt> on the page when unset.'),
|
|
118
122
|
canonical_url: z.string().optional(),
|
|
119
123
|
noindex: z.boolean().optional(),
|
|
124
|
+
lastmod_override: z.string().optional().describe('Override the sitemap <lastmod>. Empty string suppresses lastmod for this page entirely.'),
|
|
125
|
+
image_sizes_default: z.string().optional().describe('Per-page default `sizes` for responsive images (e.g. "(max-width: 640px) 360px, 560px"). Overrides the site setting; a per-<img> `sizes` attr still wins. Set when this page\'s images render narrower than the generic default so the browser stops over-fetching the larger variant.'),
|
|
120
126
|
json_ld: z.string().optional(),
|
|
127
|
+
schema_type: z.string().optional().describe('Free-form Schema.org type ("Service", "Course", "Product", …) used for auto JSON-LD emission. Use service.* for Service-specific fields.'),
|
|
128
|
+
service: z
|
|
129
|
+
.object({
|
|
130
|
+
price: z.union([z.number(), z.string()]).optional(),
|
|
131
|
+
price_currency: z.string().optional().describe('ISO 4217 (SEK, EUR, USD).'),
|
|
132
|
+
duration: z.string().optional(),
|
|
133
|
+
description: z.string().optional(),
|
|
134
|
+
url: z.string().optional(),
|
|
135
|
+
})
|
|
136
|
+
.optional(),
|
|
121
137
|
template: z.string().optional(),
|
|
122
138
|
})
|
|
123
139
|
.passthrough(),
|
package/dist/tools/settings.js
CHANGED
|
@@ -10,7 +10,7 @@ import { ok, withErrorBoundary } from './helpers.js';
|
|
|
10
10
|
export const settingsTools = [
|
|
11
11
|
{
|
|
12
12
|
name: 'read_site_settings',
|
|
13
|
-
description: "Read every site setting: name, tagline, logo, favicon, colors, fonts, contact info, social links, default SEO suffix, language, robots_txt, plus the scriptable surfaces scripts_head, scripts_body_end, and custom_css.",
|
|
13
|
+
description: "Read every site setting: name, tagline, logo, favicon, colors, fonts, contact info, social links, default SEO suffix, language, robots_txt, image_sizes_default, plus the scriptable surfaces scripts_head, scripts_body_end, and custom_css.",
|
|
14
14
|
handler: withErrorBoundary(async (_args, { client, siteId }) => {
|
|
15
15
|
const res = await client.get(siteId, 'settings');
|
|
16
16
|
return ok(res);
|
|
@@ -32,6 +32,7 @@ export const settingsTools = [
|
|
|
32
32
|
default_seo_suffix: z.string().optional(),
|
|
33
33
|
language: z.string().optional().describe('BCP-47 tag (e.g. "en", "sv", "en-GB"). Drives <html lang> on the rendered site.'),
|
|
34
34
|
robots_txt: z.string().optional(),
|
|
35
|
+
image_sizes_default: z.string().optional().describe('Site-wide default `sizes` attribute for responsive-image <picture> output, e.g. "(max-width: 640px) 360px, 560px". Tells the browser how wide images actually render so it stops over-fetching the larger srcset variant. A page can override via its own image_sizes_default; a per-<img> `sizes` attribute wins over both. Leave unset for the generic "(max-width: 768px) 100vw, 800px".'),
|
|
35
36
|
// Scriptable surfaces. Trusted because the caller has an API key.
|
|
36
37
|
scripts_head: z.string().optional().describe('Raw HTML injected into <head> on every page. Use for analytics, fonts, third-party CSS links.'),
|
|
37
38
|
scripts_body_end: z.string().optional().describe('Raw HTML injected just before </body> on every page. Use for chat widgets, deferred analytics.'),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@typeroll/mcp-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Model Context Protocol server for the Typeroll public API. Use with Claude Code or any MCP-compatible client to manage a Typeroll site.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|