UNPKG

@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
/** * 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