@extenshi/mcp
Version:
Extenshi MCP server — bring the extension catalog (search, security, market research) and pre-publish scanning into your AI tools
192 lines • 9.92 kB
JavaScript
/**
* Multi-store publishing for the MCP `publish_extension` tool.
*
* Thin wrapper over @extenshi/publisher: credentials come from the MCP
* client's environment (same env vars as `extenshi publish`), the upload
* goes straight from this machine to the store APIs. FREE — no Extenshi
* API key, nothing passes through Extenshi servers.
*/
import * as fs from 'node:fs';
import path from 'node:path';
import { isPlaceholderGeckoId, isValidGeckoId } from '@extenshi/contracts';
import { ChromeAPI, EdgeAPI, FirefoxAPI, publishToStores, } from '@extenshi/publisher';
import { assertValidArtifact } from './validate-artifact.js';
export const ENV_DOCS = {
chrome: ['CHROME_APP_ID', 'CHROME_CLIENT_ID', 'CHROME_CLIENT_SECRET', 'CHROME_REFRESH_TOKEN'],
firefox: ['FIREFOX_ADDON_GUID', 'FIREFOX_JWT_ISSUER', 'FIREFOX_JWT_SECRET'],
edge: ['EDGE_PRODUCT_ID', 'EDGE_CLIENT_ID', 'EDGE_CLIENT_SECRET', 'EDGE_TENANT_ID'],
};
/**
* Store credentials, with an explicitly stated AMO add-on id filling
* FIREFOX_ADDON_GUID when the environment has none. A different environment
* value is refused: one run publishes to one add-on id.
*/
export function storeCredentialsFor(addonId, env = process.env) {
if (addonId && env.FIREFOX_ADDON_GUID && env.FIREFOX_ADDON_GUID !== addonId)
throw new PublishSetupError(`addon_id (${addonId}) differs from FIREFOX_ADDON_GUID (${env.FIREFOX_ADDON_GUID}); state one add-on id.`);
return readStoreCredentials(addonId && !env.FIREFOX_ADDON_GUID ? { ...env, FIREFOX_ADDON_GUID: addonId } : env);
}
export function readStoreCredentials(env = process.env) {
const creds = {};
if (env.CHROME_APP_ID && env.CHROME_CLIENT_ID && env.CHROME_CLIENT_SECRET && env.CHROME_REFRESH_TOKEN) {
creds.chrome = {
appId: env.CHROME_APP_ID,
clientId: env.CHROME_CLIENT_ID,
clientSecret: env.CHROME_CLIENT_SECRET,
refreshToken: env.CHROME_REFRESH_TOKEN,
};
}
if (env.FIREFOX_ADDON_GUID && env.FIREFOX_JWT_ISSUER && env.FIREFOX_JWT_SECRET) {
creds.firefox = {
addonGuid: env.FIREFOX_ADDON_GUID,
jwtIssuer: env.FIREFOX_JWT_ISSUER,
jwtSecret: env.FIREFOX_JWT_SECRET,
};
}
if (env.EDGE_PRODUCT_ID && env.EDGE_CLIENT_ID && env.EDGE_CLIENT_SECRET && env.EDGE_TENANT_ID) {
creds.edge = {
productId: env.EDGE_PRODUCT_ID,
clientId: env.EDGE_CLIENT_ID,
clientSecret: env.EDGE_CLIENT_SECRET,
tenantId: env.EDGE_TENANT_ID,
};
}
return creds;
}
export function credentialsHelp(stores) {
return stores.map((s) => ` ${s}: ${ENV_DOCS[s].join(', ')}`).join('\n');
}
export class PublishSetupError extends Error {
}
/**
* Whether this call may CREATE a new AMO add-on. With a project, its
* owner-decided `amo.addonId` governs (and a different FIREFOX_ADDON_GUID is
* refused). Without one, `allowNewAddon` is the switch and `addonId` has to
* state the id explicitly — it is never taken silently from the environment.
* Mirrors the CLI's gate.
*/
export function amoCreateAllowed(opts) {
if (opts.addonId !== undefined && opts.addonId !== opts.addonGuid)
throw new PublishSetupError(`addon_id (${opts.addonId}) differs from FIREFOX_ADDON_GUID (${opts.addonGuid}); state one add-on id.`);
if (!opts.projectId) {
if (!opts.allowNewAddon)
return false;
if (!opts.addonId)
throw new PublishSetupError('allow_new_addon without project_id needs addon_id: the AMO add-on id to create, stated explicitly. ' +
'It becomes permanent at the first upload and is never invented. With project_id the owner decides it in Dojo instead.');
if (!isValidGeckoId(opts.addonId))
throw new PublishSetupError(`addon_id ${opts.addonId} is not an AMO add-on id: use a braced GUID ({xxxxxxxx-xxxx-…}) or name@domain.`);
if (isPlaceholderGeckoId(opts.addonId))
throw new PublishSetupError(`addon_id ${opts.addonId} is a placeholder; AMO would bind the new add-on to it forever. State the real id.`);
return true;
}
if (opts.decision?.status === 'decided') {
if (opts.decision.value !== opts.addonGuid)
throw new PublishSetupError(`FIREFOX_ADDON_GUID (${opts.addonGuid}) differs from the add-on id the project owner decided (${String(opts.decision.value)}). ` +
'Set the manifest gecko.id and FIREFOX_ADDON_GUID to the decided id; AMO never renames an add-on.');
return true;
}
if (opts.allowNewAddon)
throw new PublishSetupError(`The owner of project ${opts.projectId} has not decided amo.addonId, and creating the AMO add-on fixes it permanently. ` +
'Propose it with propose_decision; the owner decides it on the Dojo project page.');
return false;
}
/** Load the listing, source and screenshots for a listed AMO submission. */
export function loadFirefoxListing(listing) {
if (typeof FirefoxAPI.submitListing !== 'function' || typeof FirefoxAPI.loadAmoListing !== 'function')
throw new PublishSetupError("This installation's @extenshi/publisher predates AMO listing submission. Reinstall @extenshi/mcp to update it.");
try {
const parsed = FirefoxAPI.loadAmoListing(path.resolve(listing.listingPath));
const sourceFilePath = listing.sourcePath ? path.resolve(listing.sourcePath) : undefined;
if (sourceFilePath) {
if (!fs.existsSync(sourceFilePath))
throw new Error(`Source archive not found: ${listing.sourcePath}`);
FirefoxAPI.assertSourceArchive(sourceFilePath);
}
const screenshotPaths = listing.screenshotsDir
? FirefoxAPI.listScreenshots(path.resolve(listing.screenshotsDir), parsed.defaultLocale)
: undefined;
if (screenshotPaths && !screenshotPaths.length)
throw new Error(`No .png, .jpg or .gif screenshots in ${listing.screenshotsDir}`);
return {
listing: parsed,
sourceFilePath,
screenshotPaths,
allowCreate: listing.allowCreate,
replaceScreenshots: listing.replaceScreenshots ?? false,
};
}
catch (err) {
if (err instanceof PublishSetupError)
throw err;
throw new PublishSetupError(err instanceof Error ? err.message : String(err));
}
}
export async function publishArtifact(args) {
if (!fs.existsSync(args.artifactPath)) {
throw new PublishSetupError(`Artifact not found: ${args.artifactPath}`);
}
if (args.firefoxArtifactPath && !fs.existsSync(args.firefoxArtifactPath)) {
throw new PublishSetupError(`Firefox artifact not found: ${args.firefoxArtifactPath}`);
}
// Refuse to upload anything that isn't a real extension package.
try {
assertValidArtifact(args.artifactPath);
if (args.firefoxArtifactPath)
assertValidArtifact(args.firefoxArtifactPath);
}
catch (err) {
throw new PublishSetupError(err instanceof Error ? err.message : String(err));
}
const creds = storeCredentialsFor(args.addonId);
const configured = ['chrome', 'firefox', 'edge'].filter((s) => creds[s]);
const stores = args.stores?.length ? args.stores : configured;
if (!stores.length) {
throw new PublishSetupError('No store credentials found in the environment. Publishing is free and runs locally — ' +
'add credentials for at least one store to the MCP server env:\n' +
credentialsHelp(['chrome', 'firefox', 'edge']));
}
const missing = stores.filter((s) => !creds[s]);
if (missing.length) {
throw new PublishSetupError(`Missing credentials for: ${missing.join(', ')}\n${credentialsHelp(missing)}`);
}
if (args.firefoxListing && !stores.includes('firefox'))
throw new PublishSetupError('listing_path, source_path and screenshots_dir apply to Firefox (AMO); include firefox in stores.');
const firefoxListing = args.firefoxListing ? loadFirefoxListing(args.firefoxListing) : undefined;
return publishToStores({
extensionId: creds.chrome?.appId ?? creds.firefox?.addonGuid ?? creds.edge?.productId ?? 'extension',
stores,
packagePaths: {
chrome: stores.includes('chrome') ? args.artifactPath : undefined,
firefox: stores.includes('firefox') ? (args.firefoxArtifactPath ?? args.artifactPath) : undefined,
edge: stores.includes('edge') ? args.artifactPath : undefined,
},
storeConfigs: {
chrome: creds.chrome,
firefox: creds.firefox ? { ...creds.firefox, ...(firefoxListing ?? {}) } : undefined,
edge: creds.edge,
},
releaseNotes: args.releaseNotes,
parallel: true,
});
}
export async function validateStoreCredentials(stores) {
const creds = readStoreCredentials();
const targets = stores?.length ? stores : ['chrome', 'firefox', 'edge'];
return Promise.all(targets.map(async (store) => {
if (store === 'chrome' && creds.chrome) {
const valid = await ChromeAPI.validateCredentials(creds.chrome.clientId, creds.chrome.clientSecret, creds.chrome.refreshToken);
return { store, configured: true, valid };
}
if (store === 'firefox' && creds.firefox) {
const valid = await FirefoxAPI.validateCredentials(creds.firefox.jwtIssuer, creds.firefox.jwtSecret);
return { store, configured: true, valid };
}
if (store === 'edge' && creds.edge) {
const valid = await EdgeAPI.validateCredentials(creds.edge.clientId, creds.edge.clientSecret, creds.edge.tenantId);
return { store, configured: true, valid };
}
return { store, configured: false, valid: false };
}));
}
//# sourceMappingURL=publish.js.map