UNPKG

@extenshi/mcp

Version:

Extenshi MCP server — bring the extension catalog (search, security, market research) and pre-publish scanning into your AI tools

136 lines • 5.97 kB
/** * Compact reads of the hosted landing page (DX report 2026-10-10 §2 row 15). * * `get_landing_page` returns the form the live version was rendered from. With * eleven locales and an inline logo that form is ~80 KB — more than an MCP * response carries. So the tool answers with a summary by default (what is * live, which locales and sections exist, how big each part is) and returns * the form only on request, optionally narrowed to some top-level fields and * some translations. * * Shaped here, from the response the BFF already sends, rather than in the BFF: * every BFF version returns the same full form, so this works against an old * BFF and a new one alike, and the BFF contract does not change. */ import { z } from 'zod'; import { normalizeLandingLocale } from './landing-sections.js'; export const LANDING_READ_VIEWS = ['summary', 'full']; /** get_landing_page parameters besides projectId. */ export const LANDING_READ_SHAPE = { view: z .enum(LANDING_READ_VIEWS) .optional() .describe('"summary" (default): what is live and how big each part of the form is. "full": the form itself.'), fields: z .array(z.string().max(40)) .max(40) .optional() .describe('With view "full": top-level form keys to return, e.g. ["sections", "seo"] (names and sizes are in formSummary.fields). Default: all.'), locales: z .array(z.string().max(35)) .max(100) .optional() .describe('With view "full": translations to keep in form.locales, e.g. ["de"]; [] drops them all. The base content is part of every full read.'), }; const isObject = (value) => Boolean(value) && typeof value === 'object' && !Array.isArray(value); const bytesOf = (value) => value === undefined ? 0 : new TextEncoder().encode(JSON.stringify(value)).length; const LINK_FIELDS = [ 'homepageUrl', 'chromeUrl', 'firefoxUrl', 'edgeUrl', 'privacyPolicyUrl', 'supportUrl', 'uninstallUrl', 'termsUrl', ]; /** `bytes` is measured like every other size here: the field as JSON (quotes included). */ function logoOf(form) { if (typeof form.logoSvg === 'string' && form.logoSvg) return { kind: 'svg', bytes: bytesOf(form.logoSvg) }; if (typeof form.logoUrl === 'string' && form.logoUrl) return { kind: form.logoUrl.startsWith('data:') ? 'data-url' : 'url', bytes: bytesOf(form.logoUrl) }; return { kind: 'none', bytes: 0 }; } /** What an agent decides from before asking for the form itself. */ export function summarizeLandingForm(form) { const sections = Array.isArray(form.sections) ? form.sections : []; const locales = isObject(form.locales) ? form.locales : {}; const links = Object.fromEntries(LINK_FIELDS.flatMap((key) => (typeof form[key] === 'string' && form[key] ? [[key, form[key]]] : []))); return { bytes: bytesOf(form), schemaVersion: form.schemaVersion ?? 1, extensionName: form.extensionName, tagline: form.tagline, theme: form.theme, accentColor: form.accentColor, defaultLocale: normalizeLandingLocale(form.defaultLocale) ?? 'en', translations: Object.entries(locales).map(([locale, content]) => ({ locale, bytes: bytesOf(content) })), sections: sections.map((section, index) => { const s = isObject(section) ? section : {}; return { index, id: s.id, type: s.type, ...(s.hidden ? { hidden: true } : {}), bytes: bytesOf(section), }; }), links, logo: logoOf(form), features: Array.isArray(form.features) ? form.features.length : 0, screenshots: Array.isArray(form.screenshots) ? form.screenshots.length : 0, // Largest first: the parts worth leaving out of a full read. fields: Object.keys(form) .map((field) => ({ field, bytes: bytesOf(form[field]) })) .sort((a, b) => b.bytes - a.bytes), }; } function narrowForm(form, fields, locales) { let out = form; if (fields?.length) { // schemaVersion decides how the rest reads, so it always comes along; asking // for translations by `locales` keeps `locales` even when `fields` omits it. const keep = new Set([...fields, 'schemaVersion', ...(locales ? ['locales'] : [])]); out = Object.fromEntries(Object.entries(out).filter(([key]) => keep.has(key))); } if (locales && isObject(out.locales)) { const wanted = new Set(locales.map((l) => normalizeLandingLocale(l) ?? l)); out = { ...out, locales: Object.fromEntries(Object.entries(out.locales).filter(([key]) => wanted.has(normalizeLandingLocale(key) ?? key))), }; } return out; } /** * Shape a `getLandingPage` response for the agent. Anything that is not the * expected `{ page, url, form }` object passes through untouched. */ export function shapeLandingRead(result, options = {}) { if (!isObject(result) || !('form' in result)) return result; const form = isObject(result.form) ? result.form : null; if ((options.view ?? 'summary') === 'full') { const narrowed = form ? narrowForm(form, options.fields, options.locales) : result.form; return { ...result, form: narrowed, ...(form && narrowed !== form ? { formBytes: { full: bytesOf(form), returned: bytesOf(narrowed) } } : {}), }; } const { form: _form, ...rest } = result; return { ...rest, view: 'summary', formSummary: form ? summarizeLandingForm(form) : null, ...(form ? { next: 'Pass view "full" for the form; add fields (top-level keys from formSummary.fields) and locales to read only part of it.', } : {}), }; } //# sourceMappingURL=landing-read.js.map