@extenshi/mcp 0.10.0 → 0.11.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 +22 -4
- package/dist/bff.d.ts +87 -0
- package/dist/bff.d.ts.map +1 -1
- package/dist/bff.js +17 -0
- package/dist/bff.js.map +1 -1
- package/dist/development-guide.d.ts +34 -33
- package/dist/development-guide.d.ts.map +1 -1
- package/dist/development-guide.js +35 -9
- package/dist/development-guide.js.map +1 -1
- package/dist/landing-page.d.ts +22 -4
- package/dist/landing-page.d.ts.map +1 -1
- package/dist/landing-page.js +14 -11
- package/dist/landing-page.js.map +1 -1
- package/dist/landing-sections.d.ts +332 -0
- package/dist/landing-sections.d.ts.map +1 -0
- package/dist/landing-sections.js +1071 -0
- package/dist/landing-sections.js.map +1 -0
- package/dist/landing-workflow.d.ts +18 -0
- package/dist/landing-workflow.d.ts.map +1 -1
- package/dist/landing-workflow.js +56 -5
- package/dist/landing-workflow.js.map +1 -1
- package/dist/pay.d.ts +25 -1
- package/dist/pay.d.ts.map +1 -1
- package/dist/pay.js +22 -0
- package/dist/pay.js.map +1 -1
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +559 -5
- package/dist/tools.js.map +1 -1
- package/package.json +11 -11
package/dist/tools.js
CHANGED
|
@@ -40,6 +40,7 @@ import { buildDevelopmentGuide, GUIDE_SECTIONS } from './development-guide.js';
|
|
|
40
40
|
import { DEFAULT_EXCERPT_CHARS, DocsError, getDocsIndex, MAX_EXCERPT_CHARS, MIN_EXCERPT_CHARS, searchDocs, } from './docs.js';
|
|
41
41
|
import { renderIconWorkflow } from './icon-workflow.js';
|
|
42
42
|
import { LANDING_LIMITS } from './landing-page.js';
|
|
43
|
+
import { LANDING_FONTS, LANDING_SECTION_TYPES, LANDING_UI_DEFAULTS, LANDING_V2_LIMITS, } from './landing-sections.js';
|
|
43
44
|
import { hostedLandingNextSteps, landingFormFromArgs, renderGenerateLandingPage } from './landing-workflow.js';
|
|
44
45
|
import { renderLocalizeWorkflow } from './localize-workflow.js';
|
|
45
46
|
import { PAY_OPERATIONS, paySchemas } from './pay.js';
|
|
@@ -95,7 +96,7 @@ export const SERVER_INSTRUCTIONS = 'Extenshi helps develop and operate browser e
|
|
|
95
96
|
'Read get_decisions early: the AMO add-on id, license, seller identity and pricing are owner decisions — ' +
|
|
96
97
|
'propose defaults with propose_decision; the owner decides them in Dojo, and release readiness blocks on them. ' +
|
|
97
98
|
'Use list_extension_templates before the manifest, ' +
|
|
98
|
-
'and get_project_scaffold for a new project. Preserve existing source. For an existing extension, read get_project_workspace then preview and apply import_manifest to fill Dojo from its real manifest and
|
|
99
|
+
'and get_project_scaffold for a new project. Preserve existing source. For an existing extension, read get_project_workspace then preview and apply import_manifest to fill Dojo from its real manifest and its _locales messages (messagesByLocale) before configuring services. Review the integration diff and preserve local edits; write integration.file ' +
|
|
99
100
|
'verbatim to integration.path, check integration.unwired and re-read state after cabinet edits. ' +
|
|
100
101
|
'Check get_credit_balance before metered work. get_risk_by_store_ids covers up to 40 store IDs ' +
|
|
101
102
|
'for one read; get_security costs three reads for detailed findings. Hosted policy tools require ' +
|
|
@@ -584,6 +585,58 @@ const TOOL_ANNOTATIONS = {
|
|
|
584
585
|
idempotentHint: true,
|
|
585
586
|
openWorldHint: true,
|
|
586
587
|
},
|
|
588
|
+
draft_landing_page: {
|
|
589
|
+
title: 'Draft a homepage from project state',
|
|
590
|
+
readOnlyHint: true,
|
|
591
|
+
idempotentHint: true,
|
|
592
|
+
openWorldHint: true,
|
|
593
|
+
},
|
|
594
|
+
preview_landing_page: {
|
|
595
|
+
title: 'Preview and validate a homepage',
|
|
596
|
+
readOnlyHint: true,
|
|
597
|
+
idempotentHint: true,
|
|
598
|
+
openWorldHint: true,
|
|
599
|
+
},
|
|
600
|
+
list_landing_page_versions: {
|
|
601
|
+
title: 'List homepage versions',
|
|
602
|
+
readOnlyHint: true,
|
|
603
|
+
idempotentHint: true,
|
|
604
|
+
openWorldHint: true,
|
|
605
|
+
},
|
|
606
|
+
rollback_landing_page: {
|
|
607
|
+
title: 'Restore an earlier homepage version',
|
|
608
|
+
readOnlyHint: false,
|
|
609
|
+
idempotentHint: false,
|
|
610
|
+
destructiveHint: false,
|
|
611
|
+
openWorldHint: true,
|
|
612
|
+
},
|
|
613
|
+
get_custom_domain: {
|
|
614
|
+
title: 'Read the custom domain setup',
|
|
615
|
+
readOnlyHint: true,
|
|
616
|
+
idempotentHint: true,
|
|
617
|
+
openWorldHint: true,
|
|
618
|
+
},
|
|
619
|
+
set_custom_domain: {
|
|
620
|
+
title: 'Add a custom domain',
|
|
621
|
+
readOnlyHint: false,
|
|
622
|
+
idempotentHint: true,
|
|
623
|
+
destructiveHint: false,
|
|
624
|
+
openWorldHint: true,
|
|
625
|
+
},
|
|
626
|
+
verify_custom_domain: {
|
|
627
|
+
title: 'Verify a custom domain',
|
|
628
|
+
readOnlyHint: false,
|
|
629
|
+
idempotentHint: true,
|
|
630
|
+
destructiveHint: false,
|
|
631
|
+
openWorldHint: true,
|
|
632
|
+
},
|
|
633
|
+
remove_custom_domain: {
|
|
634
|
+
title: 'Remove the custom domain',
|
|
635
|
+
readOnlyHint: false,
|
|
636
|
+
idempotentHint: true,
|
|
637
|
+
destructiveHint: true,
|
|
638
|
+
openWorldHint: true,
|
|
639
|
+
},
|
|
587
640
|
unpublish_landing_page: {
|
|
588
641
|
title: 'Unpublish the hosted landing page',
|
|
589
642
|
readOnlyHint: false,
|
|
@@ -591,6 +644,52 @@ const TOOL_ANNOTATIONS = {
|
|
|
591
644
|
destructiveHint: true,
|
|
592
645
|
openWorldHint: true,
|
|
593
646
|
},
|
|
647
|
+
get_install_instructions: {
|
|
648
|
+
title: 'Read the hosted install instructions',
|
|
649
|
+
readOnlyHint: true,
|
|
650
|
+
idempotentHint: true,
|
|
651
|
+
openWorldHint: true,
|
|
652
|
+
},
|
|
653
|
+
publish_install_instructions: {
|
|
654
|
+
title: 'Publish install instructions on extenshi.io',
|
|
655
|
+
readOnlyHint: false,
|
|
656
|
+
idempotentHint: true,
|
|
657
|
+
destructiveHint: false,
|
|
658
|
+
openWorldHint: true,
|
|
659
|
+
},
|
|
660
|
+
unpublish_install_instructions: {
|
|
661
|
+
title: 'Unpublish the hosted install instructions',
|
|
662
|
+
readOnlyHint: false,
|
|
663
|
+
idempotentHint: true,
|
|
664
|
+
destructiveHint: true,
|
|
665
|
+
openWorldHint: true,
|
|
666
|
+
},
|
|
667
|
+
get_page_translations: {
|
|
668
|
+
title: 'Read uninstall / welcome page translations',
|
|
669
|
+
readOnlyHint: true,
|
|
670
|
+
idempotentHint: true,
|
|
671
|
+
openWorldHint: true,
|
|
672
|
+
},
|
|
673
|
+
set_page_translations: {
|
|
674
|
+
title: 'Save translations of an uninstall / welcome page',
|
|
675
|
+
readOnlyHint: false,
|
|
676
|
+
idempotentHint: true,
|
|
677
|
+
destructiveHint: false,
|
|
678
|
+
openWorldHint: true,
|
|
679
|
+
},
|
|
680
|
+
get_legal_translations: {
|
|
681
|
+
title: 'Read privacy policy / license terms translations',
|
|
682
|
+
readOnlyHint: true,
|
|
683
|
+
idempotentHint: true,
|
|
684
|
+
openWorldHint: true,
|
|
685
|
+
},
|
|
686
|
+
set_legal_translations: {
|
|
687
|
+
title: 'Save translations of the privacy policy / license terms',
|
|
688
|
+
readOnlyHint: false,
|
|
689
|
+
idempotentHint: true,
|
|
690
|
+
destructiveHint: false,
|
|
691
|
+
openWorldHint: true,
|
|
692
|
+
},
|
|
594
693
|
upload_project_media: {
|
|
595
694
|
title: 'Upload a public image for hosted pages',
|
|
596
695
|
readOnlyHint: false,
|
|
@@ -606,6 +705,120 @@ const TOOL_ANNOTATIONS = {
|
|
|
606
705
|
openWorldHint: true,
|
|
607
706
|
},
|
|
608
707
|
};
|
|
708
|
+
const hex = z.string().max(9);
|
|
709
|
+
const palette = z
|
|
710
|
+
.object({
|
|
711
|
+
accent: hex.optional(),
|
|
712
|
+
accentText: hex.optional(),
|
|
713
|
+
background: hex.optional(),
|
|
714
|
+
surface: hex.optional(),
|
|
715
|
+
text: hex.optional(),
|
|
716
|
+
muted: hex.optional(),
|
|
717
|
+
border: hex.optional(),
|
|
718
|
+
})
|
|
719
|
+
.optional();
|
|
720
|
+
const FONT_IDS = Object.keys(LANDING_FONTS);
|
|
721
|
+
/**
|
|
722
|
+
* Schema v2 of the landing form (shared-types/landing-sections.ts): a full,
|
|
723
|
+
* declarative homepage. Only read when schemaVersion is 2; a form without it
|
|
724
|
+
* renders exactly as before. Section contents are checked by the shared
|
|
725
|
+
* landingFormProblems, so the errors here, in the CLI and on the server match.
|
|
726
|
+
*/
|
|
727
|
+
const LANDING_V2_SHAPE = {
|
|
728
|
+
schemaVersion: z
|
|
729
|
+
.literal(2)
|
|
730
|
+
.optional()
|
|
731
|
+
.describe('2 builds a full homepage from sections, theme tokens, SEO and locales. Absent: the classic ' +
|
|
732
|
+
'one-screen page (unchanged output). draft_landing_page returns a complete v2 form.'),
|
|
733
|
+
sections: z
|
|
734
|
+
.array(z
|
|
735
|
+
.object({
|
|
736
|
+
id: z
|
|
737
|
+
.string()
|
|
738
|
+
.max(LANDING_V2_LIMITS.sectionId)
|
|
739
|
+
.describe('Lowercase slug, unique; also the locale override key.'),
|
|
740
|
+
type: z.enum(LANDING_SECTION_TYPES),
|
|
741
|
+
hidden: z.boolean().optional().describe('Keep in the form, leave out of the page.'),
|
|
742
|
+
title: z.string().max(LANDING_V2_LIMITS.title).optional(),
|
|
743
|
+
intro: z.string().max(LANDING_V2_LIMITS.intro).optional(),
|
|
744
|
+
})
|
|
745
|
+
.passthrough())
|
|
746
|
+
.max(LANDING_V2_LIMITS.sections)
|
|
747
|
+
.optional()
|
|
748
|
+
.describe('Ordered page sections (v2). Fields per type: ' +
|
|
749
|
+
'hero {headline?, subheadline?, showBadges?}; about {body — plain text, blank line = new paragraph}; ' +
|
|
750
|
+
'features {items: [{title, description?}]}; screenshots {items: [{url, alt, caption?}]} (hosted: ' +
|
|
751
|
+
'upload_project_media URLs or store screenshots); how-it-works {steps: [{title, description?}]}; ' +
|
|
752
|
+
'pricing {plans: [{name?, price?, period? one-time|month|year|lifetime, description?, features?, ' +
|
|
753
|
+
"highlighted?, offerSku?, ctaLabel?, ctaUrl?}], note?} — offerSku binds the plan to the project's " +
|
|
754
|
+
'Extenshi Pay offer so price and period come from the live offer; faq {items: [{question, answer}]} ' +
|
|
755
|
+
'(also emitted as FAQPage JSON-LD); testimonials {items: [{quote, author, role?, sourceUrl, ' +
|
|
756
|
+
'sourceLabel?}]} — real, attributed quotes only: author and a public https sourceUrl are required; ' +
|
|
757
|
+
'store-badges {browsers?: [chrome|firefox|edge]}; changelog {entries: [{version, ' +
|
|
758
|
+
'date? YYYY-MM-DD, notes: [string]}]}; links {items?: [{label, url}], includeProjectLinks?}. ' +
|
|
759
|
+
'Plain text only everywhere — no HTML, no scripts.'),
|
|
760
|
+
tokens: z
|
|
761
|
+
.object({
|
|
762
|
+
mode: z
|
|
763
|
+
.enum(['light', 'dark', 'auto'])
|
|
764
|
+
.optional()
|
|
765
|
+
.describe('auto ships both palettes (prefers-color-scheme).'),
|
|
766
|
+
light: palette,
|
|
767
|
+
dark: palette,
|
|
768
|
+
headingFont: z.enum(FONT_IDS).optional(),
|
|
769
|
+
bodyFont: z.enum(FONT_IDS).optional(),
|
|
770
|
+
radius: z.enum(['none', 'small', 'medium', 'large']).optional(),
|
|
771
|
+
width: z.enum(['narrow', 'normal', 'wide']).optional(),
|
|
772
|
+
})
|
|
773
|
+
.optional()
|
|
774
|
+
.describe('Theme tokens (v2): hex colours per mode and fonts from a system-font allowlist (no web fonts — the ' +
|
|
775
|
+
'hosted CSP loads nothing external). Poor contrast is reported as a warning.'),
|
|
776
|
+
seo: z
|
|
777
|
+
.object({
|
|
778
|
+
title: z.string().max(LANDING_V2_LIMITS.seoTitle).optional(),
|
|
779
|
+
description: z.string().max(LANDING_V2_LIMITS.seoDescription).optional(),
|
|
780
|
+
ogImageUrl: z.string().max(LANDING_LIMITS.url).optional(),
|
|
781
|
+
twitterSite: z.string().max(16).optional(),
|
|
782
|
+
authorName: z.string().max(120).optional(),
|
|
783
|
+
noindex: z.boolean().optional(),
|
|
784
|
+
})
|
|
785
|
+
.optional()
|
|
786
|
+
.describe('SEO / Open Graph (v2): <title>, meta description, og:image, twitter:site, JSON-LD author.'),
|
|
787
|
+
defaultLocale: z.string().max(35).optional().describe('Locale of the base content (default en).'),
|
|
788
|
+
locales: z
|
|
789
|
+
.record(z.string().max(35), z
|
|
790
|
+
.object({
|
|
791
|
+
extensionName: z.string().max(LANDING_LIMITS.extensionName).optional(),
|
|
792
|
+
tagline: z.string().max(LANDING_LIMITS.tagline).optional(),
|
|
793
|
+
description: z.string().max(LANDING_LIMITS.description).optional(),
|
|
794
|
+
seo: z
|
|
795
|
+
.object({ title: z.string().optional(), description: z.string().optional() })
|
|
796
|
+
.passthrough()
|
|
797
|
+
.optional(),
|
|
798
|
+
ui: z.record(z.string(), z.string().max(120)).optional(),
|
|
799
|
+
sections: z.record(z.string(), z.record(z.string(), z.unknown())).optional(),
|
|
800
|
+
})
|
|
801
|
+
.passthrough())
|
|
802
|
+
.optional()
|
|
803
|
+
.describe('Translations (v2), keyed by locale (de, pt-BR or the _locales spelling pt_BR; hosted pages accept ' +
|
|
804
|
+
'store locales). Each is served at {url}/{locale} with hreflang alternates. sections.{id} mirrors ' +
|
|
805
|
+
"that section's shape; only strings are taken (arrays merge by index), so a translation cannot " +
|
|
806
|
+
'change structure. ui overrides built-in headings and button labels.'),
|
|
807
|
+
ui: z
|
|
808
|
+
.record(z.string(), z.string().max(LANDING_V2_LIMITS.shortText))
|
|
809
|
+
.optional()
|
|
810
|
+
.describe(`Built-in strings to override (v2): ${Object.keys(LANDING_UI_DEFAULTS).join(', ')}.`),
|
|
811
|
+
uninstallUrl: z
|
|
812
|
+
.string()
|
|
813
|
+
.max(LANDING_LIMITS.url)
|
|
814
|
+
.optional()
|
|
815
|
+
.describe('Hosted uninstall feedback form URL (footer, v2).'),
|
|
816
|
+
termsUrl: z
|
|
817
|
+
.string()
|
|
818
|
+
.max(LANDING_LIMITS.url)
|
|
819
|
+
.optional()
|
|
820
|
+
.describe('License terms & refunds URL (footer, v2).'),
|
|
821
|
+
};
|
|
609
822
|
/**
|
|
610
823
|
* LandingForm fields shared by generate_landing_page (offline HTML) and
|
|
611
824
|
* publish_landing_page (hosted on page.extenshi.io). Limits match the builder
|
|
@@ -685,6 +898,7 @@ const LANDING_FORM_SHAPE = {
|
|
|
685
898
|
.max(LANDING_LIMITS.logoUrl)
|
|
686
899
|
.optional()
|
|
687
900
|
.describe('Logo image as an https URL, a relative path, or a data:image/(png|jpeg|gif|webp);base64 URL.'),
|
|
901
|
+
...LANDING_V2_SHAPE,
|
|
688
902
|
};
|
|
689
903
|
/**
|
|
690
904
|
* Register the Extenshi tools once on a FastMCP server.
|
|
@@ -761,7 +975,7 @@ export function registerTools(server, deps) {
|
|
|
761
975
|
}
|
|
762
976
|
add({
|
|
763
977
|
name: 'import_manifest',
|
|
764
|
-
description: 'Import an existing manifest.json into the Dojo manifest editor and project labels. Input: parsed JSON
|
|
978
|
+
description: 'Import an existing manifest.json into the Dojo manifest editor and project labels. Input: parsed JSON, optional default-locale messages, and optional messagesByLocale — every _locales/<lang>/messages.json keyed by locale code (only the keys named by the __MSG_*__ fields of the manifest are needed) — whose non-default locales fill the Dojo translations of name and description. Repository translations replace editor text per field; translations the repository lacks are kept, and warnings name each replaced one. Source import fills supported fields and preserves all other JSON; built imports remain separate observations. Default dryRun=true returns changes and expectedStateHash without writing; dryRun=false with that hash applies them. Does not infer data collection, prices, or publication. Requires project.write.' +
|
|
765
979
|
PROVENANCE_HINT,
|
|
766
980
|
parameters: importManifestSchema,
|
|
767
981
|
annotations: { readOnlyHint: false, idempotentHint: true, destructiveHint: true, openWorldHint: true },
|
|
@@ -990,8 +1204,11 @@ export function registerTools(server, deps) {
|
|
|
990
1204
|
name: 'publish_landing_page',
|
|
991
1205
|
description: "Publish the project's landing page (homepage) on Extenshi hosting — the same page " +
|
|
992
1206
|
'generate_landing_page returns, rendered server-side from the form (no caller HTML is stored) and ' +
|
|
993
|
-
|
|
994
|
-
'
|
|
1207
|
+
"served at page.extenshi.io/{code}, or at the root of the project's custom domain once it is ACTIVE " +
|
|
1208
|
+
'(set_custom_domain). A schema-v2 form (schemaVersion 2; draft_landing_page returns one, ' +
|
|
1209
|
+
'preview_landing_page validates it) adds sections, theme tokens, SEO, per-locale pages at {url}/{locale} with ' +
|
|
1210
|
+
'hreflang, and llms.txt; pricing plans with offerSku take price and period from the live Pay offer. ' +
|
|
1211
|
+
'The code is permanent: republishing creates a new version at the same URL. ' +
|
|
995
1212
|
'By default the URL is also registered as the project homepage, which feeds HOMEPAGE_URL. Hosted ' +
|
|
996
1213
|
'images are limited to store screenshot URLs (from get_extension) and images uploaded in the Dojo ' +
|
|
997
1214
|
'Page generator or with upload_project_media (use its url for local screenshots or a logo); the logo ' +
|
|
@@ -1050,6 +1267,341 @@ export function registerTools(server, deps) {
|
|
|
1050
1267
|
}
|
|
1051
1268
|
},
|
|
1052
1269
|
});
|
|
1270
|
+
add({
|
|
1271
|
+
name: 'draft_landing_page',
|
|
1272
|
+
description: 'Draft a full homepage (schema v2) from what the project already knows: the manifest draft and its ' +
|
|
1273
|
+
'store translations, the published page or saved Page generator form, public project media, ' +
|
|
1274
|
+
'Extenshi Pay offers (pricing plans bound by SKU), hosted artifacts (privacy policy, uninstall ' +
|
|
1275
|
+
'feedback form, license terms, support page) and the workspace release/locales. Writes nothing. ' +
|
|
1276
|
+
'Returns JSON {form, sources (field → where it came from), todos (what only you can supply — ' +
|
|
1277
|
+
'features, how-it-works, real testimonials, translations), problems, previewUrl}. The form is ' +
|
|
1278
|
+
'accepted as-is by preview_landing_page and publish_landing_page. storeUrls fills the install ' +
|
|
1279
|
+
'buttons when the project does not know its listings yet. Requires project.read.',
|
|
1280
|
+
parameters: z.object({
|
|
1281
|
+
projectId: z.string().uuid(),
|
|
1282
|
+
storeUrls: z
|
|
1283
|
+
.object({
|
|
1284
|
+
chrome: z.string().url().max(2000).optional(),
|
|
1285
|
+
firefox: z.string().url().max(2000).optional(),
|
|
1286
|
+
edge: z.string().url().max(2000).optional(),
|
|
1287
|
+
})
|
|
1288
|
+
.optional()
|
|
1289
|
+
.describe('Store listing URLs, when the project does not have them yet.'),
|
|
1290
|
+
}),
|
|
1291
|
+
execute: async (args, context) => {
|
|
1292
|
+
try {
|
|
1293
|
+
return JSON.stringify(await bff(context).draftLandingPage(args));
|
|
1294
|
+
}
|
|
1295
|
+
catch (err) {
|
|
1296
|
+
return readError(err, missingKeyMessage);
|
|
1297
|
+
}
|
|
1298
|
+
},
|
|
1299
|
+
});
|
|
1300
|
+
add({
|
|
1301
|
+
name: 'preview_landing_page',
|
|
1302
|
+
description: 'Validate and render a landing form exactly as publish_landing_page would — same image rules, ' +
|
|
1303
|
+
'store-locale check, Pay offer prices and canonical URL — without publishing. Returns JSON ' +
|
|
1304
|
+
'{ok, problems: [{path, severity, message, fix}], html (the page for `locale`), url, locales, ' +
|
|
1305
|
+
'bytes, contentHash, llmsTxt, warnings}. `ok: false` means publish would refuse the form; every ' +
|
|
1306
|
+
'problem says what to change. `html` is byte-for-byte the page publish would serve. ' +
|
|
1307
|
+
'Requires project.read.',
|
|
1308
|
+
parameters: z.object({
|
|
1309
|
+
projectId: z.string().uuid(),
|
|
1310
|
+
locale: z
|
|
1311
|
+
.string()
|
|
1312
|
+
.max(35)
|
|
1313
|
+
.optional()
|
|
1314
|
+
.describe('Which locale page to return (default the default locale).'),
|
|
1315
|
+
...LANDING_FORM_SHAPE,
|
|
1316
|
+
}),
|
|
1317
|
+
execute: async (args, context) => {
|
|
1318
|
+
const { projectId, locale, ...fields } = args;
|
|
1319
|
+
try {
|
|
1320
|
+
return JSON.stringify(await bff(context).previewLandingPage({
|
|
1321
|
+
projectId,
|
|
1322
|
+
locale,
|
|
1323
|
+
form: landingFormFromArgs(fields),
|
|
1324
|
+
}));
|
|
1325
|
+
}
|
|
1326
|
+
catch (err) {
|
|
1327
|
+
return readError(err, missingKeyMessage);
|
|
1328
|
+
}
|
|
1329
|
+
},
|
|
1330
|
+
});
|
|
1331
|
+
add({
|
|
1332
|
+
name: 'list_landing_page_versions',
|
|
1333
|
+
description: 'Version history of the hosted landing page, newest first: versionNumber, createdAt, contentHash, ' +
|
|
1334
|
+
'reason (publish | rollback | domain — a canonical switch after the custom domain changed), ' +
|
|
1335
|
+
'restoredFromVersion, schemaVersion, locales and which one is live. Requires project.read.',
|
|
1336
|
+
parameters: z.object({ projectId: z.string().uuid() }),
|
|
1337
|
+
execute: async (args, context) => {
|
|
1338
|
+
try {
|
|
1339
|
+
return JSON.stringify(await bff(context).listLandingVersions(args));
|
|
1340
|
+
}
|
|
1341
|
+
catch (err) {
|
|
1342
|
+
return readError(err, missingKeyMessage);
|
|
1343
|
+
}
|
|
1344
|
+
},
|
|
1345
|
+
});
|
|
1346
|
+
add({
|
|
1347
|
+
name: 'rollback_landing_page',
|
|
1348
|
+
description: 'Restore an earlier version of the hosted landing page. History stays append-only: the old form ' +
|
|
1349
|
+
"is published as a NEW version, re-rendered with today's URL and Pay offer prices. Returns the " +
|
|
1350
|
+
'same JSON as publish_landing_page. Requires hosted.write.',
|
|
1351
|
+
parameters: z.object({
|
|
1352
|
+
projectId: z.string().uuid(),
|
|
1353
|
+
versionNumber: z.number().int().min(1),
|
|
1354
|
+
registerAsHomepage: z.boolean().optional(),
|
|
1355
|
+
}),
|
|
1356
|
+
execute: async (args, context) => {
|
|
1357
|
+
try {
|
|
1358
|
+
const result = (await bff(context).rollbackLandingPage(args));
|
|
1359
|
+
return JSON.stringify({ ...result, nextSteps: hostedLandingNextSteps(result) });
|
|
1360
|
+
}
|
|
1361
|
+
catch (err) {
|
|
1362
|
+
return readError(err, missingKeyMessage);
|
|
1363
|
+
}
|
|
1364
|
+
},
|
|
1365
|
+
});
|
|
1366
|
+
add({
|
|
1367
|
+
name: 'get_custom_domain',
|
|
1368
|
+
description: "The project's custom domain: hostname, status (PENDING → VERIFIED → ACTIVE; FAILED; SUSPENDED when " +
|
|
1369
|
+
'the TXT record disappeared), the exact DNS records to set, whether HTTPS for custom domains is ' +
|
|
1370
|
+
'enabled on the platform (edgeConfigured) and the next step. Only ACTIVE is served. Requires project.read.',
|
|
1371
|
+
parameters: z.object({ projectId: z.string().uuid() }),
|
|
1372
|
+
execute: async (args, context) => {
|
|
1373
|
+
try {
|
|
1374
|
+
return JSON.stringify(await bff(context).getCustomDomain(args));
|
|
1375
|
+
}
|
|
1376
|
+
catch (err) {
|
|
1377
|
+
return readError(err, missingKeyMessage);
|
|
1378
|
+
}
|
|
1379
|
+
},
|
|
1380
|
+
});
|
|
1381
|
+
add({
|
|
1382
|
+
name: 'set_custom_domain',
|
|
1383
|
+
description: 'Add a custom domain (www.example.com, or the apex) for the homepage and every hosted page of the ' +
|
|
1384
|
+
'project (Pro; one hostname per project). Returns the exact DNS records: a CNAME to ' +
|
|
1385
|
+
'hosted.extenshi.io (at an apex: CNAME flattening / ALIAS / ANAME, or use www) and a TXT ownership ' +
|
|
1386
|
+
'token that stays in place (it is re-checked). Nothing is served until verify_custom_domain confirms the token and the ' +
|
|
1387
|
+
'HTTPS certificate is issued; then the homepage is served at the domain root, its canonical and ' +
|
|
1388
|
+
'HOMEPAGE_URL switch to it. Replacing the hostname starts over. Requires hosted.write.',
|
|
1389
|
+
parameters: z.object({ projectId: z.string().uuid(), hostname: z.string().max(253) }),
|
|
1390
|
+
execute: async (args, context) => {
|
|
1391
|
+
try {
|
|
1392
|
+
return JSON.stringify(await bff(context).setCustomDomain(args));
|
|
1393
|
+
}
|
|
1394
|
+
catch (err) {
|
|
1395
|
+
return readError(err, missingKeyMessage);
|
|
1396
|
+
}
|
|
1397
|
+
},
|
|
1398
|
+
});
|
|
1399
|
+
add({
|
|
1400
|
+
name: 'verify_custom_domain',
|
|
1401
|
+
description: 'Check the custom domain now: looks up the TXT token, requests or refreshes the HTTPS certificate, ' +
|
|
1402
|
+
'and moves the status (PENDING/FAILED → VERIFIED → ACTIVE). When it becomes ACTIVE the landing page ' +
|
|
1403
|
+
'is republished with the new canonical and HOMEPAGE_URL moves to https://{hostname}/ (the ' +
|
|
1404
|
+
'integration file in get_project_state follows). Safe to call repeatedly; DNS changes can take ' +
|
|
1405
|
+
'minutes. The platform also re-checks every 6 hours. Requires hosted.write.',
|
|
1406
|
+
parameters: z.object({ projectId: z.string().uuid() }),
|
|
1407
|
+
execute: async (args, context) => {
|
|
1408
|
+
try {
|
|
1409
|
+
return JSON.stringify(await bff(context).verifyCustomDomain(args));
|
|
1410
|
+
}
|
|
1411
|
+
catch (err) {
|
|
1412
|
+
return readError(err, missingKeyMessage);
|
|
1413
|
+
}
|
|
1414
|
+
},
|
|
1415
|
+
});
|
|
1416
|
+
add({
|
|
1417
|
+
name: 'remove_custom_domain',
|
|
1418
|
+
description: 'Remove the custom domain: it stops being served, the certificate is released, and the landing ' +
|
|
1419
|
+
'canonical and HOMEPAGE_URL move back to page.extenshi.io/{code}. Remove the DNS records afterwards. ' +
|
|
1420
|
+
'Requires hosted.write.',
|
|
1421
|
+
parameters: z.object({ projectId: z.string().uuid() }),
|
|
1422
|
+
execute: async (args, context) => {
|
|
1423
|
+
try {
|
|
1424
|
+
return JSON.stringify(await bff(context).removeCustomDomain(args));
|
|
1425
|
+
}
|
|
1426
|
+
catch (err) {
|
|
1427
|
+
return readError(err, missingKeyMessage);
|
|
1428
|
+
}
|
|
1429
|
+
},
|
|
1430
|
+
});
|
|
1431
|
+
add({
|
|
1432
|
+
name: 'get_install_instructions',
|
|
1433
|
+
description: 'The hosted install instructions page of a project: {page: {publicCode, enabled, publishedAt, ' +
|
|
1434
|
+
'input, url} or null, draft, draftProblems} — `draft` is the form saved by the install-instructions ' +
|
|
1435
|
+
'tool in the cabinet; `draftProblems` lists why it cannot be published as it is. The page shows store and optional sideload steps for Chrome, Firefox and Edge in the ' +
|
|
1436
|
+
'visitor’s browser language (en, es, pt-BR, fr, de, it, ru, ja, ko, zh-CN, ar), the visitor’s own ' +
|
|
1437
|
+
'browser first. Free. Requires project.read.',
|
|
1438
|
+
parameters: z.object({ projectId: z.string().uuid() }),
|
|
1439
|
+
execute: async (args, context) => {
|
|
1440
|
+
try {
|
|
1441
|
+
return JSON.stringify(await bff(context).getInstructionsPage(args));
|
|
1442
|
+
}
|
|
1443
|
+
catch (err) {
|
|
1444
|
+
return readError(err, missingKeyMessage);
|
|
1445
|
+
}
|
|
1446
|
+
},
|
|
1447
|
+
});
|
|
1448
|
+
add({
|
|
1449
|
+
name: 'publish_install_instructions',
|
|
1450
|
+
description: 'Publish (or update) the project’s install instructions at a stable public URL ' +
|
|
1451
|
+
'(dojo.extenshi.io/instructions/<code>, or https://<custom domain>/instructions). Without `input` ' +
|
|
1452
|
+
'the form saved in the cabinet is published (a saved form that does not validate is refused with ' +
|
|
1453
|
+
'INSTRUCTIONS_INVALID and its field errors). Store URLs are the https listing links on ' +
|
|
1454
|
+
'chromewebstore.google.com, addons.mozilla.org and microsoftedge.microsoft.com; browsers without one ' +
|
|
1455
|
+
'are hidden unless none is set. The install steps are translated by Extenshi; `features` are ' +
|
|
1456
|
+
'translated with set_page_translations (surface instructions). Returns {publicCode, url}. Free. ' +
|
|
1457
|
+
'Requires hosted.write.',
|
|
1458
|
+
parameters: z.object({
|
|
1459
|
+
projectId: z.string().uuid(),
|
|
1460
|
+
input: z
|
|
1461
|
+
.object({
|
|
1462
|
+
extensionName: z.string().max(120),
|
|
1463
|
+
storeUrls: z
|
|
1464
|
+
.object({
|
|
1465
|
+
chrome: z.string().max(2048).optional(),
|
|
1466
|
+
firefox: z.string().max(2048).optional(),
|
|
1467
|
+
edge: z.string().max(2048).optional(),
|
|
1468
|
+
})
|
|
1469
|
+
.optional(),
|
|
1470
|
+
features: z.array(z.string().max(200)).max(20).optional(),
|
|
1471
|
+
includeSideload: z.boolean().optional(),
|
|
1472
|
+
supportUrl: z.string().max(2048).optional().describe('https:// URL or an email address.'),
|
|
1473
|
+
})
|
|
1474
|
+
.optional(),
|
|
1475
|
+
}),
|
|
1476
|
+
execute: async (args, context) => {
|
|
1477
|
+
try {
|
|
1478
|
+
return JSON.stringify(await bff(context).publishInstructions(args));
|
|
1479
|
+
}
|
|
1480
|
+
catch (err) {
|
|
1481
|
+
return readError(err, missingKeyMessage);
|
|
1482
|
+
}
|
|
1483
|
+
},
|
|
1484
|
+
});
|
|
1485
|
+
add({
|
|
1486
|
+
name: 'unpublish_install_instructions',
|
|
1487
|
+
description: 'Take the hosted install instructions offline. The code and URL are kept, so publishing again ' +
|
|
1488
|
+
'restores the same URL. Requires hosted.write.',
|
|
1489
|
+
parameters: z.object({ projectId: z.string().uuid() }),
|
|
1490
|
+
execute: async (args, context) => {
|
|
1491
|
+
try {
|
|
1492
|
+
return JSON.stringify(await bff(context).setInstructionsEnabled({ projectId: args.projectId, enabled: false }));
|
|
1493
|
+
}
|
|
1494
|
+
catch (err) {
|
|
1495
|
+
return readError(err, missingKeyMessage);
|
|
1496
|
+
}
|
|
1497
|
+
},
|
|
1498
|
+
});
|
|
1499
|
+
add({
|
|
1500
|
+
name: 'get_page_translations',
|
|
1501
|
+
description: "The developer-written text on the project's hosted uninstall forms, welcome pages and install " +
|
|
1502
|
+
'instructions (the features list), for ' +
|
|
1503
|
+
'translation: per page {surface, publicCode, name, defaultLocale, locales, fields [{path, source, ' +
|
|
1504
|
+
'label, max, multiline}], status per language [{locale, translated, missing, outdated}], ' +
|
|
1505
|
+
'translations}. Languages are the project’s (manifest translations + the targetLocales decision); ' +
|
|
1506
|
+
'the install instructions page offers every language its steps come in. ' +
|
|
1507
|
+
'Built-in text (buttons, reasons, footers) is translated by Extenshi into en, es, pt-BR, fr, de, ' +
|
|
1508
|
+
'it, ru, ja, ko, zh-CN and ar and is not listed. A page is shown in the user’s browser language ' +
|
|
1509
|
+
'when the project offers it, otherwise in defaultLocale; an untranslated or outdated field shows ' +
|
|
1510
|
+
'its original. Requires project.read.',
|
|
1511
|
+
parameters: z.object({ projectId: z.string().uuid() }),
|
|
1512
|
+
execute: async (args, context) => {
|
|
1513
|
+
try {
|
|
1514
|
+
return JSON.stringify(await bff(context).getPageTranslations(args));
|
|
1515
|
+
}
|
|
1516
|
+
catch (err) {
|
|
1517
|
+
return readError(err, missingKeyMessage);
|
|
1518
|
+
}
|
|
1519
|
+
},
|
|
1520
|
+
});
|
|
1521
|
+
add({
|
|
1522
|
+
name: 'set_page_translations',
|
|
1523
|
+
description: 'Save one language of one uninstall form, welcome page or install instructions page: `translations` ' +
|
|
1524
|
+
'maps field paths (from ' +
|
|
1525
|
+
'get_page_translations) to translated text; the server records which original each translates, so ' +
|
|
1526
|
+
'a later edit of the original marks it outdated. An empty string removes a translation. Unknown ' +
|
|
1527
|
+
'paths, over-long text and the default language are refused (TRANSLATIONS_INVALID). Placeholders ' +
|
|
1528
|
+
'and product names are kept as written. Returns the page’s updated translation status. Requires ' +
|
|
1529
|
+
'hosted.write.',
|
|
1530
|
+
parameters: z.object({
|
|
1531
|
+
projectId: z.string().uuid(),
|
|
1532
|
+
surface: z.enum(['uninstall', 'welcome', 'instructions']),
|
|
1533
|
+
publicCode: z.string().max(40).describe('The page code, from get_page_translations.'),
|
|
1534
|
+
locale: z.string().max(35).describe('BCP 47 tag or the _locales spelling (de, pt-BR, pt_BR).'),
|
|
1535
|
+
translations: z.record(z.string().max(80), z.string().max(2000)),
|
|
1536
|
+
}),
|
|
1537
|
+
execute: async (args, context) => {
|
|
1538
|
+
try {
|
|
1539
|
+
return JSON.stringify(await bff(context).setPageTranslations(args));
|
|
1540
|
+
}
|
|
1541
|
+
catch (err) {
|
|
1542
|
+
return readError(err, missingKeyMessage);
|
|
1543
|
+
}
|
|
1544
|
+
},
|
|
1545
|
+
});
|
|
1546
|
+
const legalDocument = z
|
|
1547
|
+
.enum(['privacy_policy', 'license_terms'])
|
|
1548
|
+
.describe('privacy_policy (hosted policy, Pro) or license_terms (Extenshi Pay license terms & refunds).');
|
|
1549
|
+
add({
|
|
1550
|
+
name: 'get_legal_translations',
|
|
1551
|
+
description: "Translated copies of the project's published privacy policy or license terms. The English " +
|
|
1552
|
+
'version is the binding text and stays at the base URL; each translation lives at ' +
|
|
1553
|
+
'<url>/<locale>, says it is a translation for convenience and links to the English original. ' +
|
|
1554
|
+
'Extenshi translates its own template text (en, es, pt-BR, fr, de, it, ru, ja, ko, zh-CN, ar); ' +
|
|
1555
|
+
'what you translate is listed in fields: `section:<id>` for a section the author edited or added ' +
|
|
1556
|
+
'(translate the whole section, heading line included) and `answer:<field>` for free-text answers ' +
|
|
1557
|
+
'inside template sentences (e.g. a retention period). A section without a current translation is ' +
|
|
1558
|
+
'shown in English. Returns {defaultLocale: "en", locales, fields, status per language, ' +
|
|
1559
|
+
'translations, urls}. Languages are the project’s (manifest translations + targetLocales). ' +
|
|
1560
|
+
'Requires a published document; privacy_policy needs project.read, license_terms pay.read.',
|
|
1561
|
+
parameters: z.object({ projectId: z.string().uuid(), document: legalDocument }),
|
|
1562
|
+
execute: async ({ projectId, document }, context) => {
|
|
1563
|
+
try {
|
|
1564
|
+
const client = bff(context);
|
|
1565
|
+
return JSON.stringify(document === 'privacy_policy'
|
|
1566
|
+
? await client.getPrivacyPolicyTranslations({ projectId })
|
|
1567
|
+
: await client.getLicenseTermsTranslations({ projectId }));
|
|
1568
|
+
}
|
|
1569
|
+
catch (err) {
|
|
1570
|
+
return readError(err, missingKeyMessage);
|
|
1571
|
+
}
|
|
1572
|
+
},
|
|
1573
|
+
});
|
|
1574
|
+
add({
|
|
1575
|
+
name: 'set_legal_translations',
|
|
1576
|
+
description: 'Save one language of the privacy policy or license terms translation: `translations` maps field ' +
|
|
1577
|
+
'paths from get_legal_translations to text. A `section:<id>` text is accepted as exactly one ' +
|
|
1578
|
+
'section: its "## " heading line first ("# " for `section:lead`) and no other heading. ' +
|
|
1579
|
+
'The server records which English text each translates, so a later edit marks it outdated; an ' +
|
|
1580
|
+
'empty string removes one. Only the project’s languages are accepted (TRANSLATIONS_INVALID ' +
|
|
1581
|
+
'otherwise). Nothing changes in the English original. privacy_policy needs hosted.write and a ' +
|
|
1582
|
+
'Pro project; license_terms needs pay.write.',
|
|
1583
|
+
parameters: z.object({
|
|
1584
|
+
projectId: z.string().uuid(),
|
|
1585
|
+
document: legalDocument,
|
|
1586
|
+
locale: z.string().max(35).describe('BCP 47 tag or the _locales spelling (de, pt-BR, pt_BR).'),
|
|
1587
|
+
translations: z.record(z.string().max(300), z.string().max(16000)),
|
|
1588
|
+
sources: z
|
|
1589
|
+
.record(z.string().max(300), z.string().max(64000))
|
|
1590
|
+
.optional()
|
|
1591
|
+
.describe('The `source` of each field as get_legal_translations returned it; a save whose English has changed since is refused (TRANSLATIONS_STALE).'),
|
|
1592
|
+
}),
|
|
1593
|
+
execute: async ({ document, ...args }, context) => {
|
|
1594
|
+
try {
|
|
1595
|
+
const client = bff(context);
|
|
1596
|
+
return JSON.stringify(document === 'privacy_policy'
|
|
1597
|
+
? await client.setPrivacyPolicyTranslations(args)
|
|
1598
|
+
: await client.setLicenseTermsTranslations(args));
|
|
1599
|
+
}
|
|
1600
|
+
catch (err) {
|
|
1601
|
+
return readError(err, missingKeyMessage);
|
|
1602
|
+
}
|
|
1603
|
+
},
|
|
1604
|
+
});
|
|
1053
1605
|
// A local filesystem exists only where local artifacts do (stdio).
|
|
1054
1606
|
const localFiles = caps.has('scan');
|
|
1055
1607
|
add({
|
|
@@ -1825,7 +2377,9 @@ export function registerTools(server, deps) {
|
|
|
1825
2377
|
'as an image, or an image URL), screenshots and the canonical homepage URL for Open Graph and ' +
|
|
1826
2378
|
'JSON-LD. Returns JSON {html, bytes, warnings, nextSteps}: warnings list input that was dropped ' +
|
|
1827
2379
|
'or clamped; nextSteps cover hosting the file on HTTPS and registering the URL with ' +
|
|
1828
|
-
'upsert_hosted_page, which feeds HOMEPAGE_URL. Extenshi does not host the page.
|
|
2380
|
+
'upsert_hosted_page, which feeds HOMEPAGE_URL. Extenshi does not host the page. With schemaVersion 2 ' +
|
|
2381
|
+
'the page is built from sections, theme tokens, SEO and locales (see the field descriptions); the ' +
|
|
2382
|
+
'result adds problems [{path, severity, message, fix}] and localePages. Static content: ' +
|
|
1829
2383
|
'no API key, no network, no credits.',
|
|
1830
2384
|
parameters: z.object(LANDING_FORM_SHAPE),
|
|
1831
2385
|
execute: async (args) => JSON.stringify(renderGenerateLandingPage(args)),
|