@extenshi/mcp 0.8.0 → 0.10.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 +11 -2
- package/dist/bff.d.ts +33 -0
- package/dist/bff.d.ts.map +1 -1
- package/dist/bff.js +7 -0
- package/dist/bff.js.map +1 -1
- package/dist/development-guide.d.ts +121 -37
- package/dist/development-guide.d.ts.map +1 -1
- package/dist/development-guide.js +97 -12
- package/dist/development-guide.js.map +1 -1
- package/dist/icon-workflow.d.ts.map +1 -1
- package/dist/icon-workflow.js +15 -2
- package/dist/icon-workflow.js.map +1 -1
- package/dist/json-ld.d.ts +89 -0
- package/dist/json-ld.d.ts.map +1 -0
- package/dist/json-ld.js +118 -0
- package/dist/json-ld.js.map +1 -0
- package/dist/landing-page.d.ts +123 -0
- package/dist/landing-page.d.ts.map +1 -0
- package/dist/landing-page.js +405 -0
- package/dist/landing-page.js.map +1 -0
- package/dist/landing-workflow.d.ts +49 -0
- package/dist/landing-workflow.d.ts.map +1 -0
- package/dist/landing-workflow.js +80 -0
- package/dist/landing-workflow.js.map +1 -0
- package/dist/pay.d.ts +8 -1
- package/dist/pay.d.ts.map +1 -1
- package/dist/pay.js +9 -0
- package/dist/pay.js.map +1 -1
- package/dist/privacy-ai-outcome.d.ts +21 -0
- package/dist/privacy-ai-outcome.d.ts.map +1 -0
- package/dist/privacy-ai-outcome.js +69 -0
- package/dist/privacy-ai-outcome.js.map +1 -0
- package/dist/project-media.d.ts +29 -0
- package/dist/project-media.d.ts.map +1 -0
- package/dist/project-media.js +72 -0
- package/dist/project-media.js.map +1 -0
- package/dist/publish.d.ts +101 -0
- package/dist/publish.d.ts.map +1 -1
- package/dist/publish.js +81 -2
- package/dist/publish.js.map +1 -1
- package/dist/scan.d.ts +7 -0
- package/dist/scan.d.ts.map +1 -1
- package/dist/scan.js +26 -2
- package/dist/scan.js.map +1 -1
- package/dist/telemetry.d.ts.map +1 -1
- package/dist/telemetry.js +3 -0
- package/dist/telemetry.js.map +1 -1
- package/dist/tools.d.ts +1 -1
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +436 -15
- package/dist/tools.js.map +1 -1
- package/package.json +3 -3
package/dist/tools.js
CHANGED
|
@@ -26,22 +26,26 @@
|
|
|
26
26
|
* list_my_projects, get_project_state, get_project_scaffold,
|
|
27
27
|
* list/get/publish/update privacy policy (hosted; Pro)
|
|
28
28
|
* 'docs' → get_development_guide, search_docs, list_extension_templates, generate_icon_workflow,
|
|
29
|
-
* generate_welcome_page_workflow, localize_workflow (free; no key)
|
|
29
|
+
* generate_welcome_page_workflow, generate_landing_page, localize_workflow (free; no key)
|
|
30
30
|
* 'scan' → scan_extension (local artifact; stdio only)
|
|
31
31
|
* 'publish' → publish_extension (local creds; stdio only)
|
|
32
32
|
*
|
|
33
33
|
* stdout is the MCP protocol channel for stdio — nothing here may write to it.
|
|
34
34
|
*/
|
|
35
|
-
import { agentEvidenceSchema, importManifestSchema, workspacePatchSchema } from '@extenshi/contracts';
|
|
35
|
+
import { agentCreateProjectSchema, agentEvidenceSchema, importManifestSchema, proposeDecisionSchema, workspacePatchSchema, } from '@extenshi/contracts';
|
|
36
36
|
import { UserError } from 'fastmcp';
|
|
37
37
|
import { z } from 'zod';
|
|
38
38
|
import { zodToJsonSchema } from 'zod-to-json-schema';
|
|
39
39
|
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
|
+
import { LANDING_LIMITS } from './landing-page.js';
|
|
43
|
+
import { hostedLandingNextSteps, landingFormFromArgs, renderGenerateLandingPage } from './landing-workflow.js';
|
|
42
44
|
import { renderLocalizeWorkflow } from './localize-workflow.js';
|
|
43
45
|
import { PAY_OPERATIONS, paySchemas } from './pay.js';
|
|
44
|
-
import {
|
|
46
|
+
import { describePrivacyAiUpdate } from './privacy-ai-outcome.js';
|
|
47
|
+
import { checkInlineImage, PROJECT_MEDIA_MAX_BYTES, PROJECT_MEDIA_MIME_TYPES, ProjectMediaInputError, readWorkspaceImage, } from './project-media.js';
|
|
48
|
+
import { amoCreateAllowed, PublishSetupError, publishArtifact, storeCredentialsFor, validateStoreCredentials, } from './publish.js';
|
|
45
49
|
import { checkPublishAccess } from './publish-access.js';
|
|
46
50
|
import { ScanError, scanArtifact } from './scan.js';
|
|
47
51
|
import { describeStoreConstraints, validateSearchFilters } from './search-filters.js';
|
|
@@ -87,7 +91,10 @@ export const SERVER_INSTRUCTIONS = 'Extenshi helps develop and operate browser e
|
|
|
87
91
|
'plan from scope and research through code, assets, privacy, CI, store release and maintenance. ' +
|
|
88
92
|
'Keep a checklist covering the whole requested lifecycle. For standalone Pay, start with list_pay_apps; create_pay_app needs no development project or repository. Request explicit pay.read/pay.write scopes, use get_pay_readiness, and leave agreement acceptance and KYC to the author in the browser. Only the publishable SDK key and public verification key belong in extension code. Use search_docs (free) for current ' +
|
|
89
93
|
'product details and exact CLI flags. With identity, call list_my_projects then get_project_state ' +
|
|
90
|
-
'to reuse the existing project and repository
|
|
94
|
+
'to reuse the existing project and repository; for a new extension, create_project makes one (idempotent). ' +
|
|
95
|
+
'Read get_decisions early: the AMO add-on id, license, seller identity and pricing are owner decisions — ' +
|
|
96
|
+
'propose defaults with propose_decision; the owner decides them in Dojo, and release readiness blocks on them. ' +
|
|
97
|
+
'Use list_extension_templates before the manifest, ' +
|
|
91
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 default-locale messages before configuring services. Review the integration diff and preserve local edits; write integration.file ' +
|
|
92
99
|
'verbatim to integration.path, check integration.unwired and re-read state after cabinet edits. ' +
|
|
93
100
|
'Check get_credit_balance before metered work. get_risk_by_store_ids covers up to 40 store IDs ' +
|
|
@@ -441,6 +448,26 @@ const TOOL_ANNOTATIONS = {
|
|
|
441
448
|
idempotentHint: true,
|
|
442
449
|
openWorldHint: false,
|
|
443
450
|
},
|
|
451
|
+
create_project: {
|
|
452
|
+
title: 'Create an extension project',
|
|
453
|
+
readOnlyHint: false,
|
|
454
|
+
idempotentHint: true,
|
|
455
|
+
destructiveHint: false,
|
|
456
|
+
openWorldHint: true,
|
|
457
|
+
},
|
|
458
|
+
get_decisions: {
|
|
459
|
+
title: 'Read owner decisions',
|
|
460
|
+
readOnlyHint: true,
|
|
461
|
+
idempotentHint: true,
|
|
462
|
+
openWorldHint: true,
|
|
463
|
+
},
|
|
464
|
+
propose_decision: {
|
|
465
|
+
title: 'Propose an owner decision',
|
|
466
|
+
readOnlyHint: false,
|
|
467
|
+
idempotentHint: true,
|
|
468
|
+
destructiveHint: false,
|
|
469
|
+
openWorldHint: true,
|
|
470
|
+
},
|
|
444
471
|
list_my_projects: {
|
|
445
472
|
title: 'List my extension projects',
|
|
446
473
|
readOnlyHint: true,
|
|
@@ -501,6 +528,12 @@ const TOOL_ANNOTATIONS = {
|
|
|
501
528
|
idempotentHint: true,
|
|
502
529
|
openWorldHint: false,
|
|
503
530
|
},
|
|
531
|
+
generate_landing_page: {
|
|
532
|
+
title: 'Generate a static landing page',
|
|
533
|
+
readOnlyHint: true,
|
|
534
|
+
idempotentHint: true,
|
|
535
|
+
openWorldHint: false,
|
|
536
|
+
},
|
|
504
537
|
scan_extension: {
|
|
505
538
|
title: 'Scan an extension package',
|
|
506
539
|
readOnlyHint: false,
|
|
@@ -538,6 +571,34 @@ const TOOL_ANNOTATIONS = {
|
|
|
538
571
|
idempotentHint: true,
|
|
539
572
|
openWorldHint: true,
|
|
540
573
|
},
|
|
574
|
+
publish_landing_page: {
|
|
575
|
+
title: 'Publish a landing page on extenshi.io',
|
|
576
|
+
readOnlyHint: false,
|
|
577
|
+
idempotentHint: false,
|
|
578
|
+
destructiveHint: false,
|
|
579
|
+
openWorldHint: true,
|
|
580
|
+
},
|
|
581
|
+
get_landing_page: {
|
|
582
|
+
title: 'Read the hosted landing page',
|
|
583
|
+
readOnlyHint: true,
|
|
584
|
+
idempotentHint: true,
|
|
585
|
+
openWorldHint: true,
|
|
586
|
+
},
|
|
587
|
+
unpublish_landing_page: {
|
|
588
|
+
title: 'Unpublish the hosted landing page',
|
|
589
|
+
readOnlyHint: false,
|
|
590
|
+
idempotentHint: true,
|
|
591
|
+
destructiveHint: true,
|
|
592
|
+
openWorldHint: true,
|
|
593
|
+
},
|
|
594
|
+
upload_project_media: {
|
|
595
|
+
title: 'Upload a public image for hosted pages',
|
|
596
|
+
readOnlyHint: false,
|
|
597
|
+
// Content-addressed: the same bytes always return the same URL.
|
|
598
|
+
idempotentHint: true,
|
|
599
|
+
destructiveHint: false,
|
|
600
|
+
openWorldHint: true,
|
|
601
|
+
},
|
|
541
602
|
remove_hosted_page: {
|
|
542
603
|
title: 'Remove a hosted page registration',
|
|
543
604
|
readOnlyHint: false,
|
|
@@ -545,6 +606,86 @@ const TOOL_ANNOTATIONS = {
|
|
|
545
606
|
openWorldHint: true,
|
|
546
607
|
},
|
|
547
608
|
};
|
|
609
|
+
/**
|
|
610
|
+
* LandingForm fields shared by generate_landing_page (offline HTML) and
|
|
611
|
+
* publish_landing_page (hosted on page.extenshi.io). Limits match the builder
|
|
612
|
+
* and the BFF, so a form valid for one is valid for the other.
|
|
613
|
+
*/
|
|
614
|
+
const LANDING_FORM_SHAPE = {
|
|
615
|
+
extensionName: z.string().min(1).max(LANDING_LIMITS.extensionName).describe('Extension display name.'),
|
|
616
|
+
tagline: z.string().max(LANDING_LIMITS.tagline).optional().describe('One-line promise under the name.'),
|
|
617
|
+
description: z
|
|
618
|
+
.string()
|
|
619
|
+
.max(LANDING_LIMITS.description)
|
|
620
|
+
.optional()
|
|
621
|
+
.describe('Short paragraph on what the extension does; also the meta description.'),
|
|
622
|
+
accentColor: z
|
|
623
|
+
.string()
|
|
624
|
+
.max(9)
|
|
625
|
+
.optional()
|
|
626
|
+
.describe('Hex accent colour for the primary button and monogram (default #5e5ce6).'),
|
|
627
|
+
theme: z.enum(['light', 'dark']).optional().describe('Page colour scheme (default dark).'),
|
|
628
|
+
chromeUrl: z
|
|
629
|
+
.string()
|
|
630
|
+
.max(LANDING_LIMITS.url)
|
|
631
|
+
.optional()
|
|
632
|
+
.describe('Chrome Web Store listing URL. Empty or omitted hides the Chrome button.'),
|
|
633
|
+
firefoxUrl: z
|
|
634
|
+
.string()
|
|
635
|
+
.max(LANDING_LIMITS.url)
|
|
636
|
+
.optional()
|
|
637
|
+
.describe('Firefox Add-ons listing URL. Empty or omitted hides the Firefox button.'),
|
|
638
|
+
edgeUrl: z
|
|
639
|
+
.string()
|
|
640
|
+
.max(LANDING_LIMITS.url)
|
|
641
|
+
.optional()
|
|
642
|
+
.describe('Edge Add-ons listing URL. Empty or omitted hides the Edge button.'),
|
|
643
|
+
privacyPolicyUrl: z
|
|
644
|
+
.string()
|
|
645
|
+
.max(LANDING_LIMITS.url)
|
|
646
|
+
.optional()
|
|
647
|
+
.describe('Privacy policy URL for the footer (for example the hosted privacy.extenshi.io page).'),
|
|
648
|
+
supportUrl: z
|
|
649
|
+
.string()
|
|
650
|
+
.max(LANDING_LIMITS.url)
|
|
651
|
+
.optional()
|
|
652
|
+
.describe('Support link for the footer: an http(s) URL, or a single mailto: address (optionally ' +
|
|
653
|
+
'?subject= with a percent-encoded value).'),
|
|
654
|
+
homepageUrl: z
|
|
655
|
+
.string()
|
|
656
|
+
.max(LANDING_LIMITS.url)
|
|
657
|
+
.optional()
|
|
658
|
+
.describe('Public https URL the page will be served from: canonical link, og:url, JSON-LD url, and the ' +
|
|
659
|
+
'base for relative screenshot paths in og:image.'),
|
|
660
|
+
features: z
|
|
661
|
+
.array(z.object({
|
|
662
|
+
title: z.string().max(LANDING_LIMITS.featureTitle),
|
|
663
|
+
description: z.string().max(LANDING_LIMITS.featureDescription).optional(),
|
|
664
|
+
}))
|
|
665
|
+
.max(LANDING_LIMITS.features)
|
|
666
|
+
.optional()
|
|
667
|
+
.describe('Feature cards; entries with an empty title are skipped.'),
|
|
668
|
+
screenshots: z
|
|
669
|
+
.array(z.object({
|
|
670
|
+
url: z.string().max(LANDING_LIMITS.url),
|
|
671
|
+
alt: z.string().max(LANDING_LIMITS.screenshotAlt).optional(),
|
|
672
|
+
}))
|
|
673
|
+
.max(LANDING_LIMITS.screenshots)
|
|
674
|
+
.optional()
|
|
675
|
+
.describe('Screenshots as https URLs (for example store screenshots from get_extension) or paths ' +
|
|
676
|
+
'relative to index.html, each with alt text.'),
|
|
677
|
+
logoSvg: z
|
|
678
|
+
.string()
|
|
679
|
+
.max(LANDING_LIMITS.logoSvgBytes)
|
|
680
|
+
.optional()
|
|
681
|
+
.describe('Inline SVG logo source. Refused (monogram shown, reason in warnings) if it contains ' +
|
|
682
|
+
'scripts, event handlers, foreignObject, animation, entity references or non-fragment hrefs.'),
|
|
683
|
+
logoUrl: z
|
|
684
|
+
.string()
|
|
685
|
+
.max(LANDING_LIMITS.logoUrl)
|
|
686
|
+
.optional()
|
|
687
|
+
.describe('Logo image as an https URL, a relative path, or a data:image/(png|jpeg|gif|webp);base64 URL.'),
|
|
688
|
+
};
|
|
548
689
|
/**
|
|
549
690
|
* Register the Extenshi tools once on a FastMCP server.
|
|
550
691
|
* Only the tools whose capability is present in `deps.capabilities` are added.
|
|
@@ -633,6 +774,63 @@ export function registerTools(server, deps) {
|
|
|
633
774
|
}
|
|
634
775
|
},
|
|
635
776
|
});
|
|
777
|
+
add({
|
|
778
|
+
name: 'create_project',
|
|
779
|
+
description: 'Create a new extension project (a Dojo workspace) owned by this identity, through the same service ' +
|
|
780
|
+
'the Dojo project wizard uses, with the same ownership and limits. Returns JSON {projectId, dojoUrl, ' +
|
|
781
|
+
'project, pendingDecisions, nextSteps}: the next steps cover binding the repository and the starter ' +
|
|
782
|
+
'files. Idempotent: a repeated request with the same idempotencyKey returns the project it already ' +
|
|
783
|
+
'created (replayed: true); the same key with different input is rejected. Requires project.write.',
|
|
784
|
+
parameters: agentCreateProjectSchema,
|
|
785
|
+
execute: async (args, context) => {
|
|
786
|
+
try {
|
|
787
|
+
return JSON.stringify(await bff(context).createProject(args), null, 2);
|
|
788
|
+
}
|
|
789
|
+
catch (err) {
|
|
790
|
+
return readError(err, missingKeyMessage);
|
|
791
|
+
}
|
|
792
|
+
},
|
|
793
|
+
});
|
|
794
|
+
add({
|
|
795
|
+
name: 'get_decisions',
|
|
796
|
+
description: 'Owner decisions for one project: the AMO add-on id, license, seller public name, support contact ' +
|
|
797
|
+
'and terms, pricing, target browsers and target locales. Each entry reports its status (undecided, ' +
|
|
798
|
+
'proposed or decided), the value, a suggested default where one is derivable (a stable {GUID} add-on ' +
|
|
799
|
+
'id or the id already in the Firefox manifest, the project browsers), who proposed or decided it and ' +
|
|
800
|
+
'when, and which releases it blocks (blocksBrowsers, blocksPayment). `pending` lists the keys the ' +
|
|
801
|
+
'owner has not decided. FREE.',
|
|
802
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
|
|
803
|
+
parameters: z.object({ projectId: z.string().uuid().describe('Project id from list_my_projects.') }),
|
|
804
|
+
execute: async (args, context) => {
|
|
805
|
+
try {
|
|
806
|
+
return JSON.stringify(await bff(context).getDecisions(args), null, 2);
|
|
807
|
+
}
|
|
808
|
+
catch (err) {
|
|
809
|
+
return readError(err, missingKeyMessage);
|
|
810
|
+
}
|
|
811
|
+
},
|
|
812
|
+
});
|
|
813
|
+
add({
|
|
814
|
+
name: 'propose_decision',
|
|
815
|
+
description: 'Propose a value for one owner decision. The proposal is stored with its rationale, proposer and ' +
|
|
816
|
+
'time; the owner accepts or edits it on the Dojo project page, which is the only place a decision ' +
|
|
817
|
+
'becomes decided. A key the owner already decided is refused (DECISION_ALREADY_DECIDED). Value ' +
|
|
818
|
+
'formats: amo.addonId {GUID} or name@domain; license an SPDX id such as MIT, or all-rights-reserved; ' +
|
|
819
|
+
'seller.publicName text; seller.supportContact an email or HTTPS URL; seller.terms an HTTPS URL; ' +
|
|
820
|
+
'pricing {model: free|paid|donations, trialDays?, plans?: subscription|lifetime|one_time}; ' +
|
|
821
|
+
'targetBrowsers an array of chrome/firefox/edge; targetLocales an array of locale codes. Idempotent ' +
|
|
822
|
+
'per idempotencyKey. Requires project.write.',
|
|
823
|
+
annotations: { readOnlyHint: false, idempotentHint: true, destructiveHint: false, openWorldHint: true },
|
|
824
|
+
parameters: proposeDecisionSchema,
|
|
825
|
+
execute: async (args, context) => {
|
|
826
|
+
try {
|
|
827
|
+
return JSON.stringify(await bff(context).proposeDecision(args), null, 2);
|
|
828
|
+
}
|
|
829
|
+
catch (err) {
|
|
830
|
+
return readError(err, missingKeyMessage);
|
|
831
|
+
}
|
|
832
|
+
},
|
|
833
|
+
});
|
|
636
834
|
const reads = [
|
|
637
835
|
[
|
|
638
836
|
'get_project_workspace',
|
|
@@ -644,7 +842,11 @@ export function registerTools(server, deps) {
|
|
|
644
842
|
'Explain readiness for the recorded browser and artifact by locale. Local or agent reports are not treated as CI attestations. Stale evidence does not pass. Requires a Pro project. ' +
|
|
645
843
|
'Readiness is measured against the release recorded in the workspace: until apply_project_patch sets patch.release ' +
|
|
646
844
|
'(browser, version, artifactDigest, manifestDigest, locales, paymentRequired, and commit or dirtyTreeDigest) it reports ' +
|
|
647
|
-
'RELEASE_NOT_RECORDED; after that, evidence counts only when it matches that release artifactDigest.'
|
|
845
|
+
'RELEASE_NOT_RECORDED; after that, evidence counts only when it matches that release artifactDigest. ' +
|
|
846
|
+
"When the project has a linked Pay application, the payment check also carries `platform`: what Extenshi's own " +
|
|
847
|
+
'payment ledger proves (checkout, license, installation activation, recovery, refund). A live-mode purchase through ' +
|
|
848
|
+
"those stages passes the check with source 'platform'; test-mode purchases are listed without passing it. Clients cannot " +
|
|
849
|
+
"record source 'platform'.",
|
|
648
850
|
'getReleaseReadiness',
|
|
649
851
|
],
|
|
650
852
|
];
|
|
@@ -784,6 +986,136 @@ export function registerTools(server, deps) {
|
|
|
784
986
|
}
|
|
785
987
|
},
|
|
786
988
|
});
|
|
989
|
+
add({
|
|
990
|
+
name: 'publish_landing_page',
|
|
991
|
+
description: "Publish the project's landing page (homepage) on Extenshi hosting — the same page " +
|
|
992
|
+
'generate_landing_page returns, rendered server-side from the form (no caller HTML is stored) and ' +
|
|
993
|
+
'served at page.extenshi.io/{code}, or https://{custom-domain}/landing when the project has an ' +
|
|
994
|
+
'active custom domain. The code is permanent: republishing creates a new version at the same URL. ' +
|
|
995
|
+
'By default the URL is also registered as the project homepage, which feeds HOMEPAGE_URL. Hosted ' +
|
|
996
|
+
'images are limited to store screenshot URLs (from get_extension) and images uploaded in the Dojo ' +
|
|
997
|
+
'Page generator or with upload_project_media (use its url for local screenshots or a logo); the logo ' +
|
|
998
|
+
'may also be an inline SVG or a raster data: URL. Returns JSON {url, publicCode, ' +
|
|
999
|
+
'versionNumber, contentHash, bytes, warnings, homepage, nextSteps}. Requires hosted.write. ' +
|
|
1000
|
+
`API reference: ${HOSTED_PAGES_DOCS}`,
|
|
1001
|
+
parameters: z.object({
|
|
1002
|
+
projectId: z.string().uuid(),
|
|
1003
|
+
registerAsHomepage: z
|
|
1004
|
+
.boolean()
|
|
1005
|
+
.optional()
|
|
1006
|
+
.describe('Register the URL as the project homepage (default true). False keeps an existing homepage.'),
|
|
1007
|
+
...LANDING_FORM_SHAPE,
|
|
1008
|
+
}),
|
|
1009
|
+
execute: async (args, context) => {
|
|
1010
|
+
const { projectId, registerAsHomepage, ...fields } = args;
|
|
1011
|
+
try {
|
|
1012
|
+
const result = (await bff(context).publishLandingPage({
|
|
1013
|
+
projectId,
|
|
1014
|
+
registerAsHomepage,
|
|
1015
|
+
form: landingFormFromArgs(fields),
|
|
1016
|
+
}));
|
|
1017
|
+
return JSON.stringify({ ...result, nextSteps: hostedLandingNextSteps(result) });
|
|
1018
|
+
}
|
|
1019
|
+
catch (err) {
|
|
1020
|
+
return readError(err, missingKeyMessage);
|
|
1021
|
+
}
|
|
1022
|
+
},
|
|
1023
|
+
});
|
|
1024
|
+
add({
|
|
1025
|
+
name: 'get_landing_page',
|
|
1026
|
+
description: "The project's hosted landing page: public code, live version, publish state, URL (null when " +
|
|
1027
|
+
'unpublished) and the form the live version was rendered from.',
|
|
1028
|
+
parameters: z.object({ projectId: z.string().uuid() }),
|
|
1029
|
+
execute: async (args, context) => {
|
|
1030
|
+
try {
|
|
1031
|
+
return JSON.stringify(await bff(context).getLandingPage(args));
|
|
1032
|
+
}
|
|
1033
|
+
catch (err) {
|
|
1034
|
+
return readError(err, missingKeyMessage);
|
|
1035
|
+
}
|
|
1036
|
+
},
|
|
1037
|
+
});
|
|
1038
|
+
add({
|
|
1039
|
+
name: 'unpublish_landing_page',
|
|
1040
|
+
description: 'Take the hosted landing page offline (it answers 404). The public code and version history are ' +
|
|
1041
|
+
'kept, so publishing again restores the same URL. A homepage registration that points at this ' +
|
|
1042
|
+
`page is removed with it. Requires hosted.write. API reference: ${HOSTED_PAGES_DOCS}`,
|
|
1043
|
+
parameters: z.object({ projectId: z.string().uuid() }),
|
|
1044
|
+
execute: async (args, context) => {
|
|
1045
|
+
try {
|
|
1046
|
+
return JSON.stringify(await bff(context).unpublishLandingPage(args));
|
|
1047
|
+
}
|
|
1048
|
+
catch (err) {
|
|
1049
|
+
return readError(err, missingKeyMessage);
|
|
1050
|
+
}
|
|
1051
|
+
},
|
|
1052
|
+
});
|
|
1053
|
+
// A local filesystem exists only where local artifacts do (stdio).
|
|
1054
|
+
const localFiles = caps.has('scan');
|
|
1055
|
+
add({
|
|
1056
|
+
name: 'upload_project_media',
|
|
1057
|
+
description: "Upload a logo or screenshot (PNG, JPEG or WebP, up to 2 MB) to the project's PUBLIC media store — " +
|
|
1058
|
+
'the same store the Dojo Page generator uploads into — and get a stable https URL that ' +
|
|
1059
|
+
'publish_landing_page accepts as screenshots[].url or logoUrl. The server checks the file by content ' +
|
|
1060
|
+
'(magic bytes), strips EXIF/XMP/text metadata and anything after the image end marker, bounds the ' +
|
|
1061
|
+
'dimensions (16–4096 px) and enforces a per-project quota (100 files / 50 MB, Dojo uploads included); ' +
|
|
1062
|
+
'identical bytes return the same URL without using quota. SVG is refused: rasterize it first ' +
|
|
1063
|
+
'(CLI: extenshi icon store-assets) or pass an inline logo as logoSvg. ' +
|
|
1064
|
+
(localFiles
|
|
1065
|
+
? 'Pass filePath (a file inside the workspace) or dataBase64 + mime. '
|
|
1066
|
+
: 'Pass dataBase64 + mime (this connection has no access to local files). ') +
|
|
1067
|
+
'Store screenshots come from the CLI: extenshi screenshots. Returns JSON {url, sha256, mime, bytes, ' +
|
|
1068
|
+
'width, height, strippedBytes, deduplicated, quota}. Requires hosted.write. ' +
|
|
1069
|
+
`CLI: extenshi media upload <file> --project <id>. API reference: ${HOSTED_PAGES_DOCS}`,
|
|
1070
|
+
parameters: z.object({
|
|
1071
|
+
projectId: z.string().uuid(),
|
|
1072
|
+
filePath: z
|
|
1073
|
+
.string()
|
|
1074
|
+
.min(1)
|
|
1075
|
+
.max(4096)
|
|
1076
|
+
.optional()
|
|
1077
|
+
.describe('Image file inside the workspace (local stdio server only).'),
|
|
1078
|
+
dataBase64: z
|
|
1079
|
+
.string()
|
|
1080
|
+
.min(1)
|
|
1081
|
+
.max(Math.ceil(PROJECT_MEDIA_MAX_BYTES / 3) * 4 + 16)
|
|
1082
|
+
.optional()
|
|
1083
|
+
.describe('The raw image, base64-encoded (no data: prefix). Use with mime.'),
|
|
1084
|
+
mime: z.enum(PROJECT_MEDIA_MIME_TYPES).optional().describe('Required with dataBase64.'),
|
|
1085
|
+
}),
|
|
1086
|
+
execute: async (args, context) => {
|
|
1087
|
+
let payload;
|
|
1088
|
+
try {
|
|
1089
|
+
if (args.filePath && args.dataBase64)
|
|
1090
|
+
throw new ProjectMediaInputError('Pass filePath OR dataBase64, not both.');
|
|
1091
|
+
if (args.filePath) {
|
|
1092
|
+
if (!localFiles)
|
|
1093
|
+
throw new ProjectMediaInputError('filePath works only with the local stdio server; send dataBase64 + mime instead.');
|
|
1094
|
+
const read = await readWorkspaceImage(args.filePath);
|
|
1095
|
+
payload = { mime: read.mime, dataBase64: read.dataBase64 };
|
|
1096
|
+
}
|
|
1097
|
+
else if (args.dataBase64) {
|
|
1098
|
+
if (!args.mime)
|
|
1099
|
+
throw new ProjectMediaInputError('mime is required with dataBase64.');
|
|
1100
|
+
checkInlineImage(args.dataBase64, args.mime);
|
|
1101
|
+
payload = { mime: args.mime, dataBase64: args.dataBase64 };
|
|
1102
|
+
}
|
|
1103
|
+
else
|
|
1104
|
+
throw new ProjectMediaInputError('Pass filePath or dataBase64 + mime.');
|
|
1105
|
+
}
|
|
1106
|
+
catch (err) {
|
|
1107
|
+
if (err instanceof ProjectMediaInputError)
|
|
1108
|
+
throw new UserError(err.message);
|
|
1109
|
+
throw err;
|
|
1110
|
+
}
|
|
1111
|
+
try {
|
|
1112
|
+
return JSON.stringify(await bff(context).uploadProjectMedia({ projectId: args.projectId, ...payload }));
|
|
1113
|
+
}
|
|
1114
|
+
catch (err) {
|
|
1115
|
+
return readError(err, missingKeyMessage);
|
|
1116
|
+
}
|
|
1117
|
+
},
|
|
1118
|
+
});
|
|
787
1119
|
add({
|
|
788
1120
|
name: 'list_hosted_pages',
|
|
789
1121
|
description: 'List the project registered hosted pages (homepage/support) with their verification status, content hash and last check time.',
|
|
@@ -1320,14 +1652,17 @@ export function registerTools(server, deps) {
|
|
|
1320
1652
|
name: 'update_privacy_policy_with_ai',
|
|
1321
1653
|
description: "Propose an updated privacy policy that keeps the author's custom wording and adds sections " +
|
|
1322
1654
|
'required by new permissions or data practices. Returns proposedMarkdown only; nothing goes ' +
|
|
1323
|
-
'live.
|
|
1324
|
-
'
|
|
1655
|
+
'live. aiStep states whether the AI step ran (ran | did_not_run | discarded | not_needed) and ' +
|
|
1656
|
+
'notice says why; unless aiStep is ran, proposedMarkdown is a deterministic section merge, not an ' +
|
|
1657
|
+
'AI draft. conflictHeadings and missingCategories list what still needs a manual edit. Publishing ' +
|
|
1658
|
+
'is a separate step (publish_privacy_policy with the returned kind). Requires a policy that is ' +
|
|
1659
|
+
'already published. Pro only.',
|
|
1325
1660
|
parameters: z.object({
|
|
1326
1661
|
projectId: z.string().describe('Project id from list_my_projects.'),
|
|
1327
1662
|
}),
|
|
1328
1663
|
execute: async (args, context) => {
|
|
1329
1664
|
try {
|
|
1330
|
-
return JSON.stringify(await bff(context).updatePrivacyPolicyWithAi(args), null, 2);
|
|
1665
|
+
return JSON.stringify(describePrivacyAiUpdate(await bff(context).updatePrivacyPolicyWithAi(args)), null, 2);
|
|
1331
1666
|
}
|
|
1332
1667
|
catch (err) {
|
|
1333
1668
|
return readError(err, missingKeyMessage);
|
|
@@ -1342,13 +1677,14 @@ export function registerTools(server, deps) {
|
|
|
1342
1677
|
description: 'For extension-development tasks: returns the complete tool inventory for THIS connection, ' +
|
|
1343
1678
|
'the Extenshi service directory with access requirements and docs links, GitHub/code placement ' +
|
|
1344
1679
|
'guidance, and the ordered workflow from idea and repository through implementation, assets, ' +
|
|
1345
|
-
'privacy, tests/CI, store submission and maintenance
|
|
1346
|
-
'CLI
|
|
1680
|
+
'privacy, tests/CI, store submission and maintenance, plus store media: the scenes.json format for ' +
|
|
1681
|
+
'CLI screenshots (per-locale store screenshots) and listing artwork sizes per store. Distinguishes ' +
|
|
1682
|
+
'MCP tools from cabinet, CLI and external steps. Free; no key, account lookup or network request.',
|
|
1347
1683
|
parameters: z.object({
|
|
1348
1684
|
sections: z
|
|
1349
1685
|
.array(z.enum(['all', ...GUIDE_SECTIONS]))
|
|
1350
1686
|
.optional()
|
|
1351
|
-
.describe('Full guide sections to include: tools, localActions, services, repository, workflow, handoff, or all. ' +
|
|
1687
|
+
.describe('Full guide sections to include: tools, localActions, services, repository, workflow, handoff, storeMedia, or all. ' +
|
|
1352
1688
|
'Omitted: a compact overview with tool names, workflow stages and a table of contents.'),
|
|
1353
1689
|
}),
|
|
1354
1690
|
execute: async (args) => JSON.stringify(buildDevelopmentGuide(registeredTools, args?.sections), null, 2),
|
|
@@ -1479,6 +1815,21 @@ export function registerTools(server, deps) {
|
|
|
1479
1815
|
accentColor: args.accent_color,
|
|
1480
1816
|
}),
|
|
1481
1817
|
});
|
|
1818
|
+
add({
|
|
1819
|
+
name: 'generate_landing_page',
|
|
1820
|
+
description: 'Generate a static landing page (homepage) for a browser extension as one self-contained HTML ' +
|
|
1821
|
+
'file: no JavaScript, escaped text, http(s)-only links (the support link may also be a mailto: ' +
|
|
1822
|
+
'address), and store buttons only for the store ' +
|
|
1823
|
+
'URLs given. Same generator as the cabinet Page generator, so a saved `page-generator` form ' +
|
|
1824
|
+
'from get_project_state maps field for field. Optional logo (inline SVG, validated and embedded ' +
|
|
1825
|
+
'as an image, or an image URL), screenshots and the canonical homepage URL for Open Graph and ' +
|
|
1826
|
+
'JSON-LD. Returns JSON {html, bytes, warnings, nextSteps}: warnings list input that was dropped ' +
|
|
1827
|
+
'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. Static content: ' +
|
|
1829
|
+
'no API key, no network, no credits.',
|
|
1830
|
+
parameters: z.object(LANDING_FORM_SHAPE),
|
|
1831
|
+
execute: async (args) => JSON.stringify(renderGenerateLandingPage(args)),
|
|
1832
|
+
});
|
|
1482
1833
|
add({
|
|
1483
1834
|
name: 'list_extension_templates',
|
|
1484
1835
|
description: 'The kinds of browser extension you can build, with the permissions each one REQUIRES: ' +
|
|
@@ -1541,7 +1892,12 @@ export function registerTools(server, deps) {
|
|
|
1541
1892
|
}
|
|
1542
1893
|
},
|
|
1543
1894
|
});
|
|
1544
|
-
|
|
1895
|
+
// The scan report goes out as-is (defanged + labelled as data: finding
|
|
1896
|
+
// text quotes the artifact's own code). It used to be passed through
|
|
1897
|
+
// shapeExtension — the catalog-DETAIL shaper — whose field list has
|
|
1898
|
+
// none of the report's keys, so every scan came back as
|
|
1899
|
+
// {"snapshots": []} after the credit was spent.
|
|
1900
|
+
return renderCatalogPayload(report);
|
|
1545
1901
|
}
|
|
1546
1902
|
catch (err) {
|
|
1547
1903
|
// Keep the ScanError as `cause`: scanErrorMessage() renders 402/429
|
|
@@ -1564,8 +1920,12 @@ export function registerTools(server, deps) {
|
|
|
1564
1920
|
'CHROME_REFRESH_TOKEN, FIREFOX_ADDON_GUID/FIREFOX_JWT_ISSUER/FIREFOX_JWT_SECRET, ' +
|
|
1565
1921
|
'EDGE_PRODUCT_ID/EDGE_CLIENT_ID/EDGE_CLIENT_SECRET/EDGE_TENANT_ID). The upload itself is local, but ' +
|
|
1566
1922
|
'publishing is in an active testing phase: a quick Extenshi access check runs first (it identifies your ' +
|
|
1567
|
-
'account by EXTENSHI_API_KEY). Edge submissions are polled to a terminal status.
|
|
1568
|
-
'
|
|
1923
|
+
'account by EXTENSHI_API_KEY). Edge submissions are polled to a terminal status. For Firefox, ' +
|
|
1924
|
+
'listing_path turns the upload into a listed AMO submission with metadata: first-time add-on creation ' +
|
|
1925
|
+
'(gated by allow_new_addon or the project decision), per-locale name/summary/description, categories, ' +
|
|
1926
|
+
'license, homepage, support email and URL, privacy policy, release and reviewer notes, reviewer source ' +
|
|
1927
|
+
'(source_path) and previews (screenshots_dir); the result lists each step. The artifact is not ' +
|
|
1928
|
+
'security-scanned by this tool (scanning is scan_extension).',
|
|
1569
1929
|
parameters: z.object({
|
|
1570
1930
|
artifact_path: z
|
|
1571
1931
|
.string()
|
|
@@ -1579,6 +1939,42 @@ export function registerTools(server, deps) {
|
|
|
1579
1939
|
.optional()
|
|
1580
1940
|
.describe('Separate Firefox artifact (.xpi); defaults to artifact_path.'),
|
|
1581
1941
|
release_notes: z.string().optional().describe('Release notes for stores that accept them.'),
|
|
1942
|
+
listing_path: z
|
|
1943
|
+
.string()
|
|
1944
|
+
.optional()
|
|
1945
|
+
.describe('Firefox: AMO listing file (JSON with per-locale fields, or the AMO.md that generate-listing --store ' +
|
|
1946
|
+
'firefox writes, placeholders filled). Makes the Firefox submission a listed one with metadata: ' +
|
|
1947
|
+
'the add-on is created on first submission (see allow_new_addon) or its listing is edited, then ' +
|
|
1948
|
+
'privacy policy, release notes, reviewer notes, source and previews are applied.'),
|
|
1949
|
+
source_path: z
|
|
1950
|
+
.string()
|
|
1951
|
+
.optional()
|
|
1952
|
+
.describe('Firefox: reviewer source archive (.zip/.tar.gz/.tgz/.tar.bz2) for the new version.'),
|
|
1953
|
+
screenshots_dir: z
|
|
1954
|
+
.string()
|
|
1955
|
+
.optional()
|
|
1956
|
+
.describe('Firefox: AMO previews in file-name order: a directory of .png/.jpg/.gif files, or the extenshi ' +
|
|
1957
|
+
'screenshots output directory (its firefox/<locale>/ set, default locale first).'),
|
|
1958
|
+
allow_new_addon: z
|
|
1959
|
+
.boolean()
|
|
1960
|
+
.optional()
|
|
1961
|
+
.describe('Firefox: permits creating a NEW AMO add-on, whose id becomes permanent. With project_id the ' +
|
|
1962
|
+
"project's decided amo.addonId governs creation instead."),
|
|
1963
|
+
addon_id: z
|
|
1964
|
+
.string()
|
|
1965
|
+
.max(255)
|
|
1966
|
+
.optional()
|
|
1967
|
+
.describe('Firefox: the AMO add-on id ({GUID} or name@domain), stated explicitly. Needed with allow_new_addon ' +
|
|
1968
|
+
'when there is no project_id; fills FIREFOX_ADDON_GUID when the environment has none.'),
|
|
1969
|
+
replace_screenshots: z
|
|
1970
|
+
.boolean()
|
|
1971
|
+
.optional()
|
|
1972
|
+
.describe('Firefox: replaces the previews of an existing AMO add-on with screenshots_dir.'),
|
|
1973
|
+
project_id: z
|
|
1974
|
+
.string()
|
|
1975
|
+
.uuid()
|
|
1976
|
+
.optional()
|
|
1977
|
+
.describe('Dojo project id. Its owner-decided amo.addonId gates first-time AMO creation and has to equal FIREFOX_ADDON_GUID.'),
|
|
1582
1978
|
extension_id: z
|
|
1583
1979
|
.number()
|
|
1584
1980
|
.optional()
|
|
@@ -1598,7 +1994,7 @@ export function registerTools(server, deps) {
|
|
|
1598
1994
|
// preflight evaluates the `publish-access` PostHog flag for this developer
|
|
1599
1995
|
// / extension. Fails open on any transport error — only a definitive
|
|
1600
1996
|
// server "no" blocks here (see checkPublishAccess).
|
|
1601
|
-
const creds =
|
|
1997
|
+
const creds = storeCredentialsFor(args.addon_id);
|
|
1602
1998
|
const storeIds = {};
|
|
1603
1999
|
if (creds.chrome)
|
|
1604
2000
|
storeIds.chrome = creds.chrome.appId;
|
|
@@ -1616,12 +2012,37 @@ export function registerTools(server, deps) {
|
|
|
1616
2012
|
throw new UserError(access.message ??
|
|
1617
2013
|
"Publishing is in an active testing phase and isn't available for your account yet.");
|
|
1618
2014
|
}
|
|
2015
|
+
let firefoxListing;
|
|
2016
|
+
if (args.listing_path || args.source_path || args.screenshots_dir) {
|
|
2017
|
+
if (!args.listing_path)
|
|
2018
|
+
throw new UserError('source_path and screenshots_dir need listing_path, the AMO listing they belong to.');
|
|
2019
|
+
let decision = null;
|
|
2020
|
+
if (args.project_id) {
|
|
2021
|
+
const view = (await bff(context).getDecisions({ projectId: args.project_id }));
|
|
2022
|
+
decision = view.decisions?.find((d) => d.key === 'amo.addonId') ?? null;
|
|
2023
|
+
}
|
|
2024
|
+
firefoxListing = {
|
|
2025
|
+
listingPath: args.listing_path,
|
|
2026
|
+
sourcePath: args.source_path,
|
|
2027
|
+
screenshotsDir: args.screenshots_dir,
|
|
2028
|
+
replaceScreenshots: args.replace_screenshots,
|
|
2029
|
+
allowCreate: amoCreateAllowed({
|
|
2030
|
+
addonGuid: creds.firefox?.addonGuid ?? '',
|
|
2031
|
+
allowNewAddon: args.allow_new_addon,
|
|
2032
|
+
addonId: args.addon_id,
|
|
2033
|
+
projectId: args.project_id,
|
|
2034
|
+
decision,
|
|
2035
|
+
}),
|
|
2036
|
+
};
|
|
2037
|
+
}
|
|
1619
2038
|
void context.reportProgress({ progress: 0, total: 1 });
|
|
1620
2039
|
const result = await publishArtifact({
|
|
1621
2040
|
artifactPath: args.artifact_path,
|
|
1622
2041
|
stores: args.stores,
|
|
1623
2042
|
firefoxArtifactPath: args.firefox_artifact_path,
|
|
1624
2043
|
releaseNotes: args.release_notes,
|
|
2044
|
+
addonId: args.addon_id,
|
|
2045
|
+
firefoxListing,
|
|
1625
2046
|
});
|
|
1626
2047
|
void context.reportProgress({ progress: 1, total: 1 });
|
|
1627
2048
|
return JSON.stringify(result, null, 2);
|