@extenshi/mcp
Version:
Extenshi MCP server — bring the extension catalog (search, security, market research) and pre-publish scanning into your AI tools
151 lines • 8.51 kB
JavaScript
/**
* Body of the free `generate_landing_page` MCP tool.
*
* Pure function of its arguments — no API key, no network, no credits — so it
* is registered under the `docs` capability and works on both the local stdio
* server and the hosted connector. The HTML comes from ./landing-page.ts, a
* synced copy of shared-types/landing-page.ts: the page an agent generates here
* is byte-for-byte what the cabinet Page generator downloads for the same form.
*
* This tool does not host the page (publish_landing_page does, on
* page.extenshi.io). The returned `nextSteps` describe hosting it on any HTTPS
* origin and registering the URL with `upsert_hosted_page`, which is what feeds
* HOMEPAGE_URL in the generated integration config.
*/
import { DEFAULT_LANDING_FORM, landingFormToInput, landingPageWarnings, } from './landing-page.js';
import { landingFormProblems, renderLandingLocales, } from './landing-sections.js';
const V2_KEYS = [
'sections',
'tokens',
'seo',
'defaultLocale',
'locales',
'ui',
'uninstallUrl',
'termsUrl',
];
export function landingFormFromArgs(args) {
const form = {
...DEFAULT_LANDING_FORM,
extensionName: args.extensionName,
tagline: args.tagline ?? '',
description: args.description ?? '',
accentColor: args.accentColor ?? DEFAULT_LANDING_FORM.accentColor,
theme: args.theme ?? DEFAULT_LANDING_FORM.theme,
chromeUrl: args.chromeUrl ?? '',
firefoxUrl: args.firefoxUrl ?? '',
edgeUrl: args.edgeUrl ?? '',
privacyPolicyUrl: args.privacyPolicyUrl ?? '',
supportUrl: args.supportUrl ?? '',
features: (args.features ?? []).map((f) => ({ title: f.title, description: f.description ?? '' })),
};
if (args.homepageUrl)
form.homepageUrl = args.homepageUrl;
if (args.logoUrl)
form.logoUrl = args.logoUrl;
if (args.logoSvg)
form.logoSvg = args.logoSvg;
if (args.screenshots?.length) {
form.screenshots = args.screenshots.map((s) => ({ url: s.url, alt: s.alt ?? '' }));
}
if (args.schemaVersion === 2)
form.schemaVersion = 2;
// Passed through as given — landingFormProblems reports anything malformed,
// including v2 keys sent without schemaVersion 2.
for (const key of V2_KEYS) {
if (args[key] !== undefined)
form[key] = args[key];
}
return form;
}
export function landingNextSteps(homepageUrl) {
return [
'Save `html` verbatim as index.html. Put any relative screenshot or logo paths next to it at the same relative locations.',
'Host the file on a public HTTPS origin — for example GitHub Pages (commit index.html to the repository, then Settings → Pages → deploy from the branch and folder), Cloudflare Pages, Netlify or any static host. The page has no JavaScript, so it needs no build step or CSP changes.',
homepageUrl
? `Confirm the page loads at ${homepageUrl} (the canonical/og:url tags already point there).`
: 'Once the final URL is known, regenerate with homepageUrl set to it so the canonical, og:url and JSON-LD url tags are filled in, and re-upload.',
'Register the URL on the project: upsert_hosted_page {projectId, kind: "homepage", url} (needs identity and hosted.write; the server fetches the public page and hashes it, so it has to be publicly reachable). CLI equivalent: npx @extenshi/cli@latest page register --project <id> --url <https-url>.',
'get_project_state now returns a regenerated integration.file (the content for integration.path) that carries the new HOMEPAGE_URL; updating the repository with it is the author’s choice.',
'After later edits to the hosted page, verify_hosted_artifact {projectId, kind: "homepage"} reports whether the live page still matches the registered hash.',
];
}
export function renderGenerateLandingPage(args) {
const form = landingFormFromArgs(args);
const input = landingFormToInput(form);
const pages = renderLandingLocales(form);
const html = pages[0]?.html ?? '';
const homepage = /^https:\/\/\S+$/i.test(input.homepageUrl ?? '') ? input.homepageUrl : undefined;
const nextSteps = landingNextSteps(homepage);
if (form.schemaVersion !== 2) {
const problems = landingFormProblems(form).filter((p) => p.severity === 'error');
return {
html,
bytes: new TextEncoder().encode(html).length,
warnings: landingPageWarnings(input),
...(problems.length ? { problems } : {}),
nextSteps,
};
}
const problems = landingFormProblems(form);
const localePages = pages
.slice(1)
.map((p) => ({ locale: p.locale, path: `${p.slug}/index.html`, html: p.html }));
if (localePages.length) {
nextSteps.splice(1, 0, `Save each localePages[].html at its path (${localePages.map((p) => p.path).join(', ')}) next to index.html — hreflang links point at those URLs.`);
}
if (problems.some((p) => p.severity === 'error')) {
nextSteps.unshift('Fix the problems with severity "error" first: a hosted publish refuses them, and the page drops those parts.');
}
return {
html,
bytes: new TextEncoder().encode(html).length,
warnings: problems.filter((p) => p.severity === 'warning').map((p) => `${p.path}: ${p.message}`),
problems,
...(localePages.length ? { localePages } : {}),
nextSteps,
};
}
/**
* Follow-ups after publish_landing_page, from the fields the BFF returned.
* "Live" is said only when the server fetched the public URL and got the
* published bytes back (`live.status: verified`); a BFF without the check
* (older deploy) is reported as unverified, never as live.
*/
export function hostedLandingNextSteps(result) {
const homepage = (result.homepage ?? {});
const live = (result.live ?? null);
const url = String(result.url);
const why = live?.detail ? ` (${live.detail})` : '';
const verify = 'verify_hosted_artifact {projectId, kind: "homepage"} re-checks it (CLI: extenshi page verify); the version is stored and goes live as soon as the URL serves it.';
const reachable = live?.status === 'verified';
const steps = live?.status === 'verified'
? [`The page is live at ${url}; the same URL keeps serving every later version.`]
: live?.status === 'unreachable'
? [
`PUBLISHED BUT UNREACHABLE: ${url} does not serve the page${why}. Do not tell the user it is live. ${verify}`,
]
: live?.status === 'changed'
? [`PUBLISHED BUT NOT SERVED: ${url} answers with different content${why}. ${verify}`]
: [`Published at ${url}, not verified (this server does not check the public URL). ${verify}`];
steps.push(homepage.registered
? reachable
? 'It is now the project homepage. get_project_state returns an integration.file (the content for integration.path) that uses it as HOMEPAGE_URL; the repository is updated with it only when the author wants that.'
: 'It is registered as the project homepage, but until the URL serves the page HOMEPAGE_URL would send users to an error; get_project_state returns an integration.file that carries that URL.'
: 'The homepage registration was left unchanged (registerAsHomepage: false).');
if (reachable)
steps.push('verify_hosted_artifact {projectId, kind: "homepage"} confirms the public URL serves the published bytes (the hash is contentHash).');
if (homepage.previousUrl)
steps.push(`This replaced the previously registered homepage ${homepage.previousUrl}.`);
const warnings = Array.isArray(result.warnings) ? result.warnings : [];
if (warnings.length)
steps.push('Review warnings: those values were clamped or dropped in the rendered page.');
const locales = Array.isArray(result.locales) ? result.locales : [];
if (locales.length > 1)
steps.push(`Locale pages: ${locales.map((l) => `${l.locale} ${l.url}`).join(', ')}.`);
if (typeof result.llmsTxtUrl === 'string')
steps.push(`llms.txt is served at ${result.llmsTxtUrl}.`);
steps.push('Every publish is a new version: list_landing_page_versions shows the history and rollback_landing_page restores one. set_custom_domain serves this page at your own domain root (Pro).');
return steps;
}
//# sourceMappingURL=landing-workflow.js.map