@extenshi/mcp 0.7.0 → 0.9.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 +9 -4
- package/dist/bff.d.ts +17 -0
- package/dist/bff.d.ts.map +1 -1
- package/dist/bff.js +4 -0
- package/dist/bff.js.map +1 -1
- package/dist/development-guide.d.ts +109 -79
- package/dist/development-guide.d.ts.map +1 -1
- package/dist/development-guide.js +133 -45
- package/dist/development-guide.js.map +1 -1
- package/dist/docs.d.ts +45 -4
- package/dist/docs.d.ts.map +1 -1
- package/dist/docs.js +162 -17
- package/dist/docs.js.map +1 -1
- package/dist/icon-workflow.d.ts +5 -4
- package/dist/icon-workflow.d.ts.map +1 -1
- package/dist/icon-workflow.js +33 -32
- 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/localize-workflow.d.ts +6 -1
- package/dist/localize-workflow.d.ts.map +1 -1
- package/dist/localize-workflow.js +65 -47
- package/dist/localize-workflow.js.map +1 -1
- package/dist/pay.d.ts +1 -1
- package/dist/pay.d.ts.map +1 -1
- package/dist/pay.js +11 -11
- 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/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/search-filters.js +1 -1
- package/dist/search-filters.js.map +1 -1
- package/dist/shape.d.ts +34 -0
- package/dist/shape.d.ts.map +1 -1
- package/dist/shape.js +150 -3
- package/dist/shape.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/templates.js +5 -5
- package/dist/templates.js.map +1 -1
- package/dist/tools.d.ts +2 -1
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +398 -115
- package/dist/tools.js.map +1 -1
- package/dist/welcome-agent-brief.d.ts +9 -9
- package/dist/welcome-agent-brief.d.ts.map +1 -1
- package/dist/welcome-agent-brief.js +32 -32
- package/dist/welcome-agent-brief.js.map +1 -1
- package/dist/welcome-workflow.d.ts +5 -5
- package/dist/welcome-workflow.d.ts.map +1 -1
- package/dist/welcome-workflow.js +13 -14
- package/dist/welcome-workflow.js.map +1 -1
- package/package.json +3 -2
package/dist/tools.js
CHANGED
|
@@ -26,7 +26,7 @@
|
|
|
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
|
*
|
|
@@ -35,16 +35,20 @@
|
|
|
35
35
|
import { agentEvidenceSchema, importManifestSchema, workspacePatchSchema } from '@extenshi/contracts';
|
|
36
36
|
import { UserError } from 'fastmcp';
|
|
37
37
|
import { z } from 'zod';
|
|
38
|
-
import {
|
|
39
|
-
import {
|
|
38
|
+
import { zodToJsonSchema } from 'zod-to-json-schema';
|
|
39
|
+
import { buildDevelopmentGuide, GUIDE_SECTIONS } from './development-guide.js';
|
|
40
|
+
import { DEFAULT_EXCERPT_CHARS, DocsError, getDocsIndex, MAX_EXCERPT_CHARS, MIN_EXCERPT_CHARS, searchDocs, } from './docs.js';
|
|
40
41
|
import { renderIconWorkflow } from './icon-workflow.js';
|
|
42
|
+
import { LANDING_LIMITS } from './landing-page.js';
|
|
43
|
+
import { hostedLandingNextSteps, landingFormFromArgs, renderGenerateLandingPage } from './landing-workflow.js';
|
|
41
44
|
import { renderLocalizeWorkflow } from './localize-workflow.js';
|
|
42
45
|
import { PAY_OPERATIONS, paySchemas } from './pay.js';
|
|
46
|
+
import { describePrivacyAiUpdate } from './privacy-ai-outcome.js';
|
|
43
47
|
import { PublishSetupError, publishArtifact, readStoreCredentials, validateStoreCredentials, } from './publish.js';
|
|
44
48
|
import { checkPublishAccess } from './publish-access.js';
|
|
45
49
|
import { ScanError, scanArtifact } from './scan.js';
|
|
46
50
|
import { describeStoreConstraints, validateSearchFilters } from './search-filters.js';
|
|
47
|
-
import { shapeExtension, shapeReviews, shapeSearch, shapeSecurity, shapeStoreRiskBatch } from './shape.js';
|
|
51
|
+
import { DEFAULT_MARKET_TOP_N, MARKET_FACETS, MAX_MARKET_TOP_N, shapeExtension, shapeMarketOverview, shapeReviews, shapeSearch, shapeSecurity, shapeStoreRiskBatch, } from './shape.js';
|
|
48
52
|
import { captureError, captureEvent, classifyError } from './telemetry.js';
|
|
49
53
|
import { renderExtensionTemplates } from './templates.js';
|
|
50
54
|
import { renderCatalogPayload } from './untrusted.js';
|
|
@@ -53,6 +57,10 @@ import { renderWelcomeWorkflow } from './welcome-workflow.js';
|
|
|
53
57
|
export const KEY_PAGE = 'https://dojo.extenshi.io/api-keys';
|
|
54
58
|
export const SIGNUP_PAGE = 'https://auth.extenshi.io/signup';
|
|
55
59
|
export const BILLING_PAGE = 'https://dojo.extenshi.io/billing';
|
|
60
|
+
export const HOSTED_PAGES_DOCS = 'https://docs.extenshi.io/developers/project-sync#hosted-pages-from-the-api';
|
|
61
|
+
// Every workspace write carries provenance; saying so up front saves the agent a
|
|
62
|
+
// failed first call on `provenance.toolVersion: Required`.
|
|
63
|
+
const PROVENANCE_HINT = ' `provenance` is required: {source: "agent", observedAt: ISO-8601 timestamp, toolVersion: your client name and version}.';
|
|
56
64
|
export const MISSING_KEY_MESSAGE = "This tool needs an Extenshi API key, and you don't have one set up yet — " +
|
|
57
65
|
"here's how to get going.\n\n" +
|
|
58
66
|
'Getting started is free: no credit card required. Every account includes a ' +
|
|
@@ -238,12 +246,34 @@ function attribution(session) {
|
|
|
238
246
|
* strongly typed as before.
|
|
239
247
|
* Fail-soft: telemetry never alters the tool's result or its thrown error.
|
|
240
248
|
*/
|
|
249
|
+
/**
|
|
250
|
+
* Publish every tool's input schema with all `$ref`s inlined.
|
|
251
|
+
*
|
|
252
|
+
* fastmcp converts `parameters` through xsschema, which runs zod-to-json-schema
|
|
253
|
+
* with its default `$refStrategy: 'root'`: every zod instance used twice in one
|
|
254
|
+
* schema (the shared `digest`, `provenanceSchema` strings, …) becomes a bare
|
|
255
|
+
* `{"$ref": "#/properties/…"}` with no `type`. The Claude connector directory
|
|
256
|
+
* rejects those ("Add a type to this parameter: inputDigest"), and weaker MCP
|
|
257
|
+
* clients cannot resolve them either. xsschema prefers a Standard JSON Schema
|
|
258
|
+
* `jsonSchema` hook when one is present, so we supply an inlined conversion
|
|
259
|
+
* there; validation still goes through the original zod `~standard.validate`.
|
|
260
|
+
*/
|
|
261
|
+
function withInlinedJsonSchema(parameters) {
|
|
262
|
+
const input = () => zodToJsonSchema(parameters, { $refStrategy: 'none' });
|
|
263
|
+
return Object.create(parameters, {
|
|
264
|
+
'~standard': { value: { ...parameters['~standard'], jsonSchema: { input, output: input } } },
|
|
265
|
+
});
|
|
266
|
+
}
|
|
241
267
|
function instrument(tool) {
|
|
242
268
|
const { name } = tool;
|
|
243
269
|
const original = tool.execute;
|
|
244
270
|
return {
|
|
245
271
|
...tool,
|
|
246
272
|
execute: async (args, context) => {
|
|
273
|
+
// The hosted connector lets a user decline analytics on the consent
|
|
274
|
+
// screen; honour it for every event, including error reports.
|
|
275
|
+
if (context.session?.telemetry === false)
|
|
276
|
+
return original(args, context);
|
|
247
277
|
const startedAt = Date.now();
|
|
248
278
|
const { userId, sessionId } = attribution(context.session);
|
|
249
279
|
const props = { tool: name, ...(sessionId ? { mcp_session_id: sessionId } : {}) };
|
|
@@ -290,8 +320,8 @@ const EXTENSION_REF_SHAPE = {
|
|
|
290
320
|
.max(100)
|
|
291
321
|
.optional()
|
|
292
322
|
.describe('Alternative to extension_id: the extension’s id in its store URL (e.g. Chrome ' +
|
|
293
|
-
'"cjpalhdlnbpafiamejdnhcphjbkeiagm").
|
|
294
|
-
'
|
|
323
|
+
'"cjpalhdlnbpafiamejdnhcphjbkeiagm"). A Chrome/Edge id (32 letters a–p) also requires ' +
|
|
324
|
+
'`store`: both stores share that format. Firefox ids (slug / GUID / email) are ' +
|
|
295
325
|
'unambiguous. Resolving a store_id is free; only the read that follows costs a credit.'),
|
|
296
326
|
store: z
|
|
297
327
|
.enum(['CHROME', 'FIREFOX', 'EDGE'])
|
|
@@ -474,6 +504,12 @@ const TOOL_ANNOTATIONS = {
|
|
|
474
504
|
idempotentHint: true,
|
|
475
505
|
openWorldHint: false,
|
|
476
506
|
},
|
|
507
|
+
generate_landing_page: {
|
|
508
|
+
title: 'Generate a static landing page',
|
|
509
|
+
readOnlyHint: true,
|
|
510
|
+
idempotentHint: true,
|
|
511
|
+
openWorldHint: false,
|
|
512
|
+
},
|
|
477
513
|
scan_extension: {
|
|
478
514
|
title: 'Scan an extension package',
|
|
479
515
|
readOnlyHint: false,
|
|
@@ -511,6 +547,112 @@ const TOOL_ANNOTATIONS = {
|
|
|
511
547
|
idempotentHint: true,
|
|
512
548
|
openWorldHint: true,
|
|
513
549
|
},
|
|
550
|
+
publish_landing_page: {
|
|
551
|
+
title: 'Publish a landing page on extenshi.io',
|
|
552
|
+
readOnlyHint: false,
|
|
553
|
+
idempotentHint: false,
|
|
554
|
+
destructiveHint: false,
|
|
555
|
+
openWorldHint: true,
|
|
556
|
+
},
|
|
557
|
+
get_landing_page: {
|
|
558
|
+
title: 'Read the hosted landing page',
|
|
559
|
+
readOnlyHint: true,
|
|
560
|
+
idempotentHint: true,
|
|
561
|
+
openWorldHint: true,
|
|
562
|
+
},
|
|
563
|
+
unpublish_landing_page: {
|
|
564
|
+
title: 'Unpublish the hosted landing page',
|
|
565
|
+
readOnlyHint: false,
|
|
566
|
+
idempotentHint: true,
|
|
567
|
+
destructiveHint: true,
|
|
568
|
+
openWorldHint: true,
|
|
569
|
+
},
|
|
570
|
+
remove_hosted_page: {
|
|
571
|
+
title: 'Remove a hosted page registration',
|
|
572
|
+
readOnlyHint: false,
|
|
573
|
+
destructiveHint: true,
|
|
574
|
+
openWorldHint: true,
|
|
575
|
+
},
|
|
576
|
+
};
|
|
577
|
+
/**
|
|
578
|
+
* LandingForm fields shared by generate_landing_page (offline HTML) and
|
|
579
|
+
* publish_landing_page (hosted on page.extenshi.io). Limits match the builder
|
|
580
|
+
* and the BFF, so a form valid for one is valid for the other.
|
|
581
|
+
*/
|
|
582
|
+
const LANDING_FORM_SHAPE = {
|
|
583
|
+
extensionName: z.string().min(1).max(LANDING_LIMITS.extensionName).describe('Extension display name.'),
|
|
584
|
+
tagline: z.string().max(LANDING_LIMITS.tagline).optional().describe('One-line promise under the name.'),
|
|
585
|
+
description: z
|
|
586
|
+
.string()
|
|
587
|
+
.max(LANDING_LIMITS.description)
|
|
588
|
+
.optional()
|
|
589
|
+
.describe('Short paragraph on what the extension does; also the meta description.'),
|
|
590
|
+
accentColor: z
|
|
591
|
+
.string()
|
|
592
|
+
.max(9)
|
|
593
|
+
.optional()
|
|
594
|
+
.describe('Hex accent colour for the primary button and monogram (default #5e5ce6).'),
|
|
595
|
+
theme: z.enum(['light', 'dark']).optional().describe('Page colour scheme (default dark).'),
|
|
596
|
+
chromeUrl: z
|
|
597
|
+
.string()
|
|
598
|
+
.max(LANDING_LIMITS.url)
|
|
599
|
+
.optional()
|
|
600
|
+
.describe('Chrome Web Store listing URL. Empty or omitted hides the Chrome button.'),
|
|
601
|
+
firefoxUrl: z
|
|
602
|
+
.string()
|
|
603
|
+
.max(LANDING_LIMITS.url)
|
|
604
|
+
.optional()
|
|
605
|
+
.describe('Firefox Add-ons listing URL. Empty or omitted hides the Firefox button.'),
|
|
606
|
+
edgeUrl: z
|
|
607
|
+
.string()
|
|
608
|
+
.max(LANDING_LIMITS.url)
|
|
609
|
+
.optional()
|
|
610
|
+
.describe('Edge Add-ons listing URL. Empty or omitted hides the Edge button.'),
|
|
611
|
+
privacyPolicyUrl: z
|
|
612
|
+
.string()
|
|
613
|
+
.max(LANDING_LIMITS.url)
|
|
614
|
+
.optional()
|
|
615
|
+
.describe('Privacy policy URL for the footer (for example the hosted privacy.extenshi.io page).'),
|
|
616
|
+
supportUrl: z
|
|
617
|
+
.string()
|
|
618
|
+
.max(LANDING_LIMITS.url)
|
|
619
|
+
.optional()
|
|
620
|
+
.describe('Support link for the footer: an http(s) URL, or a single mailto: address (optionally ' +
|
|
621
|
+
'?subject= with a percent-encoded value).'),
|
|
622
|
+
homepageUrl: z
|
|
623
|
+
.string()
|
|
624
|
+
.max(LANDING_LIMITS.url)
|
|
625
|
+
.optional()
|
|
626
|
+
.describe('Public https URL the page will be served from: canonical link, og:url, JSON-LD url, and the ' +
|
|
627
|
+
'base for relative screenshot paths in og:image.'),
|
|
628
|
+
features: z
|
|
629
|
+
.array(z.object({
|
|
630
|
+
title: z.string().max(LANDING_LIMITS.featureTitle),
|
|
631
|
+
description: z.string().max(LANDING_LIMITS.featureDescription).optional(),
|
|
632
|
+
}))
|
|
633
|
+
.max(LANDING_LIMITS.features)
|
|
634
|
+
.optional()
|
|
635
|
+
.describe('Feature cards; entries with an empty title are skipped.'),
|
|
636
|
+
screenshots: z
|
|
637
|
+
.array(z.object({
|
|
638
|
+
url: z.string().max(LANDING_LIMITS.url),
|
|
639
|
+
alt: z.string().max(LANDING_LIMITS.screenshotAlt).optional(),
|
|
640
|
+
}))
|
|
641
|
+
.max(LANDING_LIMITS.screenshots)
|
|
642
|
+
.optional()
|
|
643
|
+
.describe('Screenshots as https URLs (for example store screenshots from get_extension) or paths ' +
|
|
644
|
+
'relative to index.html, each with alt text.'),
|
|
645
|
+
logoSvg: z
|
|
646
|
+
.string()
|
|
647
|
+
.max(LANDING_LIMITS.logoSvgBytes)
|
|
648
|
+
.optional()
|
|
649
|
+
.describe('Inline SVG logo source. Refused (monogram shown, reason in warnings) if it contains ' +
|
|
650
|
+
'scripts, event handlers, foreignObject, animation, entity references or non-fragment hrefs.'),
|
|
651
|
+
logoUrl: z
|
|
652
|
+
.string()
|
|
653
|
+
.max(LANDING_LIMITS.logoUrl)
|
|
654
|
+
.optional()
|
|
655
|
+
.describe('Logo image as an https URL, a relative path, or a data:image/(png|jpeg|gif|webp);base64 URL.'),
|
|
514
656
|
};
|
|
515
657
|
/**
|
|
516
658
|
* Register the Extenshi tools once on a FastMCP server.
|
|
@@ -530,7 +672,8 @@ export function registerTools(server, deps) {
|
|
|
530
672
|
...tool.annotations,
|
|
531
673
|
title: tool.annotations?.title ?? TOOL_ANNOTATIONS[tool.name]?.title ?? tool.name.replaceAll('_', ' '),
|
|
532
674
|
};
|
|
533
|
-
|
|
675
|
+
const parameters = tool.parameters && withInlinedJsonSchema(tool.parameters);
|
|
676
|
+
server.addTool(instrument({ ...tool, annotations, ...(parameters ? { parameters } : {}) }));
|
|
534
677
|
registeredTools.push({ name: tool.name, description: tool.description ?? '', annotations });
|
|
535
678
|
}
|
|
536
679
|
// Per-call helpers bound to the injected deps.
|
|
@@ -545,7 +688,7 @@ export function registerTools(server, deps) {
|
|
|
545
688
|
for (const [method, name, , , mutation, description] of PAY_OPERATIONS) {
|
|
546
689
|
add({
|
|
547
690
|
name,
|
|
548
|
-
description: `${description} Requires ${mutation ? 'pay.write' : 'pay.read'} permission
|
|
691
|
+
description: `${description} Requires ${mutation ? 'pay.write' : 'pay.read'} permission.`,
|
|
549
692
|
parameters: paySchemas[method],
|
|
550
693
|
annotations: {
|
|
551
694
|
readOnlyHint: !mutation,
|
|
@@ -562,13 +705,22 @@ export function registerTools(server, deps) {
|
|
|
562
705
|
if (err instanceof UserError && /^MCP_SCOPE_REQUIRED:pay\.(read|write)\./.test(err.message))
|
|
563
706
|
throw err;
|
|
564
707
|
const status = err?.data?.httpStatus;
|
|
708
|
+
// Only OUR authored backend messages are matched, never echoed, so a
|
|
709
|
+
// provider string can't leak through; they pick the precise guidance.
|
|
710
|
+
const backendMessage = err instanceof Error ? err.message : '';
|
|
565
711
|
const message = status === 403
|
|
566
712
|
? 'Pay access denied. Grant pay.read/pay.write explicitly and verify application ownership.'
|
|
567
|
-
: status === 412
|
|
568
|
-
? 'Pay
|
|
569
|
-
: status ===
|
|
570
|
-
? 'Pay
|
|
571
|
-
:
|
|
713
|
+
: status === 412 && backendMessage.includes('Pay application is archived')
|
|
714
|
+
? 'This Pay application is archived. Use an active application from list_pay_apps, or create a new one.'
|
|
715
|
+
: status === 412
|
|
716
|
+
? 'Pay prerequisites are incomplete. Read get_pay_readiness; the author completes legal acceptance and Stripe KYC in the browser.'
|
|
717
|
+
: status === 404 && backendMessage.includes('No payment gateway connected')
|
|
718
|
+
? 'No payment provider is connected to this Pay application yet. Call connect_pay_seller and have the author finish onboarding in the browser, then retry.'
|
|
719
|
+
: status === 404
|
|
720
|
+
? 'Pay application or resource not found. Check the appId with list_pay_apps.'
|
|
721
|
+
: status === 401
|
|
722
|
+
? 'Pay authentication failed. Reconnect or configure a developer API key with explicit Pay permissions.'
|
|
723
|
+
: 'Pay request failed. Check authentication, application readiness and backend support before retrying a mutation.';
|
|
572
724
|
// Provider failures may contain customer data or credentials; never echo them.
|
|
573
725
|
throw userErrorFrom(message, err);
|
|
574
726
|
}
|
|
@@ -577,7 +729,8 @@ export function registerTools(server, deps) {
|
|
|
577
729
|
}
|
|
578
730
|
add({
|
|
579
731
|
name: 'import_manifest',
|
|
580
|
-
description: 'Import an existing manifest.json into the Dojo manifest editor and project labels.
|
|
732
|
+
description: 'Import an existing manifest.json into the Dojo manifest editor and project labels. Input: parsed JSON and optional default-locale messages. Source import fills supported fields and preserves all other JSON; built imports remain separate observations. Default dryRun=true returns changes and expectedStateHash without writing; dryRun=false with that hash applies them. Does not infer data collection, prices, or publication. Requires project.write.' +
|
|
733
|
+
PROVENANCE_HINT,
|
|
581
734
|
parameters: importManifestSchema,
|
|
582
735
|
annotations: { readOnlyHint: false, idempotentHint: true, destructiveHint: true, openWorldHint: true },
|
|
583
736
|
execute: async (args, context) => {
|
|
@@ -597,7 +750,10 @@ export function registerTools(server, deps) {
|
|
|
597
750
|
],
|
|
598
751
|
[
|
|
599
752
|
'get_release_readiness',
|
|
600
|
-
'Explain readiness for the recorded browser and artifact by locale. Local or agent reports
|
|
753
|
+
'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. ' +
|
|
754
|
+
'Readiness is measured against the release recorded in the workspace: until apply_project_patch sets patch.release ' +
|
|
755
|
+
'(browser, version, artifactDigest, manifestDigest, locales, paymentRequired, and commit or dirtyTreeDigest) it reports ' +
|
|
756
|
+
'RELEASE_NOT_RECORDED; after that, evidence counts only when it matches that release artifactDigest.',
|
|
601
757
|
'getReleaseReadiness',
|
|
602
758
|
],
|
|
603
759
|
];
|
|
@@ -629,7 +785,7 @@ export function registerTools(server, deps) {
|
|
|
629
785
|
});
|
|
630
786
|
add({
|
|
631
787
|
name: 'connection_diagnostics',
|
|
632
|
-
description: 'Verify authentication, granted scopes, supported workspace contracts and actual write permissions (incl. hosted.write, evidence uploads and, for a projectId, CI ingest configuration). Includes credit balances;
|
|
788
|
+
description: 'Verify authentication, granted scopes, supported workspace contracts and actual write permissions (incl. hosted.write, evidence uploads and, for a projectId, CI ingest configuration). Includes credit balances; credentials and secrets are not included. Auth failures are recoverable with `extenshi login --recover` (CLI) or a reissued API key from dojo.extenshi.io/api-keys.',
|
|
633
789
|
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
|
|
634
790
|
parameters: z.object({}),
|
|
635
791
|
execute: async (_args, context) => {
|
|
@@ -653,8 +809,8 @@ export function registerTools(server, deps) {
|
|
|
653
809
|
add({
|
|
654
810
|
name,
|
|
655
811
|
description: dryRun
|
|
656
|
-
?
|
|
657
|
-
:
|
|
812
|
+
? `Preview a three-way metadata diff against a server-held base revision. No writes.${PROVENANCE_HINT}`
|
|
813
|
+
: `Apply a typed project metadata patch with expectedRevision and idempotencyKey. Source and built manifests are stored separately, including unknown keys. Omitted fields are left unchanged; tombstones explicitly delete. No source upload or scaffold overwrite. patch.release records the release that get_release_readiness measures against. Requires project.write OAuth scope.${PROVENANCE_HINT}`,
|
|
658
814
|
annotations: {
|
|
659
815
|
readOnlyHint: dryRun,
|
|
660
816
|
idempotentHint: true,
|
|
@@ -673,7 +829,7 @@ export function registerTools(server, deps) {
|
|
|
673
829
|
});
|
|
674
830
|
add({
|
|
675
831
|
name: 'create_ci_ingest_secret',
|
|
676
|
-
description: 'Create or rotate the per-project secret the extenshi-evidence-action uses to POST CI evidence (POST /projects/:id/evidence/ingest). The secret is
|
|
832
|
+
description: 'Create or rotate the per-project secret the extenshi-evidence-action uses to POST CI evidence (POST /projects/:id/evidence/ingest). The secret is returned only once; the action reads it from the repository GitHub Actions secret EXTENSHI_EVIDENCE_SECRET. Not idempotent: creating again invalidates the previous secret. Requires project.write.',
|
|
677
833
|
annotations: { readOnlyHint: false, idempotentHint: false, destructiveHint: true, openWorldHint: true },
|
|
678
834
|
parameters: z.object({ projectId: z.string().uuid() }),
|
|
679
835
|
execute: async (args, context) => {
|
|
@@ -687,7 +843,7 @@ export function registerTools(server, deps) {
|
|
|
687
843
|
});
|
|
688
844
|
add({
|
|
689
845
|
name: 'upsert_hosted_page',
|
|
690
|
-
description:
|
|
846
|
+
description: `Register the project's homepage or support page URL (HTTPS only; the server fetches the public page and hashes the body, so the URL has to be publicly reachable). The registered homepage URL feeds HOMEPAGE_URL when the integration config file is regenerated. Requires hosted.write. API reference: ${HOSTED_PAGES_DOCS}`,
|
|
691
847
|
annotations: { readOnlyHint: false, idempotentHint: true, destructiveHint: false, openWorldHint: true },
|
|
692
848
|
parameters: z.object({
|
|
693
849
|
projectId: z.string().uuid(),
|
|
@@ -705,7 +861,7 @@ export function registerTools(server, deps) {
|
|
|
705
861
|
});
|
|
706
862
|
add({
|
|
707
863
|
name: 'verify_hosted_artifact',
|
|
708
|
-
description:
|
|
864
|
+
description: `Verify a hosted page actually serves what its record says: re-fetches and compares content. kind=homepage|support compares against the registered body hash (verified | changed | unreachable); kind=privacy checks the published policy version's HTML is what the public page really serves. Requires hosted.write. API reference: ${HOSTED_PAGES_DOCS}`,
|
|
709
865
|
annotations: { readOnlyHint: false, idempotentHint: true, destructiveHint: false, openWorldHint: true },
|
|
710
866
|
parameters: z.object({
|
|
711
867
|
projectId: z.string().uuid(),
|
|
@@ -720,6 +876,86 @@ export function registerTools(server, deps) {
|
|
|
720
876
|
}
|
|
721
877
|
},
|
|
722
878
|
});
|
|
879
|
+
add({
|
|
880
|
+
name: 'remove_hosted_page',
|
|
881
|
+
description: `Remove the project's registered homepage or support page — for a project moving off a URL or a mis-registered address. The public page itself keeps working; only the project record is forgotten. Requires hosted.write. API reference: ${HOSTED_PAGES_DOCS}`,
|
|
882
|
+
annotations: { readOnlyHint: false, idempotentHint: true, destructiveHint: true, openWorldHint: true },
|
|
883
|
+
parameters: z.object({
|
|
884
|
+
projectId: z.string().uuid(),
|
|
885
|
+
kind: z.enum(['homepage', 'support']),
|
|
886
|
+
}),
|
|
887
|
+
execute: async (args, context) => {
|
|
888
|
+
try {
|
|
889
|
+
return JSON.stringify(await bff(context).removeHostedPage(args));
|
|
890
|
+
}
|
|
891
|
+
catch (err) {
|
|
892
|
+
return readError(err, missingKeyMessage);
|
|
893
|
+
}
|
|
894
|
+
},
|
|
895
|
+
});
|
|
896
|
+
add({
|
|
897
|
+
name: 'publish_landing_page',
|
|
898
|
+
description: "Publish the project's landing page (homepage) on Extenshi hosting — the same page " +
|
|
899
|
+
'generate_landing_page returns, rendered server-side from the form (no caller HTML is stored) and ' +
|
|
900
|
+
'served at page.extenshi.io/{code}, or https://{custom-domain}/landing when the project has an ' +
|
|
901
|
+
'active custom domain. The code is permanent: republishing creates a new version at the same URL. ' +
|
|
902
|
+
'By default the URL is also registered as the project homepage, which feeds HOMEPAGE_URL. Hosted ' +
|
|
903
|
+
'images are limited to store screenshot URLs (from get_extension) and images uploaded in the Dojo ' +
|
|
904
|
+
'Page generator; the logo may be an inline SVG or a raster data: URL. Returns JSON {url, publicCode, ' +
|
|
905
|
+
'versionNumber, contentHash, bytes, warnings, homepage, nextSteps}. Requires hosted.write. ' +
|
|
906
|
+
`API reference: ${HOSTED_PAGES_DOCS}`,
|
|
907
|
+
parameters: z.object({
|
|
908
|
+
projectId: z.string().uuid(),
|
|
909
|
+
registerAsHomepage: z
|
|
910
|
+
.boolean()
|
|
911
|
+
.optional()
|
|
912
|
+
.describe('Register the URL as the project homepage (default true). False keeps an existing homepage.'),
|
|
913
|
+
...LANDING_FORM_SHAPE,
|
|
914
|
+
}),
|
|
915
|
+
execute: async (args, context) => {
|
|
916
|
+
const { projectId, registerAsHomepage, ...fields } = args;
|
|
917
|
+
try {
|
|
918
|
+
const result = (await bff(context).publishLandingPage({
|
|
919
|
+
projectId,
|
|
920
|
+
registerAsHomepage,
|
|
921
|
+
form: landingFormFromArgs(fields),
|
|
922
|
+
}));
|
|
923
|
+
return JSON.stringify({ ...result, nextSteps: hostedLandingNextSteps(result) });
|
|
924
|
+
}
|
|
925
|
+
catch (err) {
|
|
926
|
+
return readError(err, missingKeyMessage);
|
|
927
|
+
}
|
|
928
|
+
},
|
|
929
|
+
});
|
|
930
|
+
add({
|
|
931
|
+
name: 'get_landing_page',
|
|
932
|
+
description: "The project's hosted landing page: public code, live version, publish state, URL (null when " +
|
|
933
|
+
'unpublished) and the form the live version was rendered from.',
|
|
934
|
+
parameters: z.object({ projectId: z.string().uuid() }),
|
|
935
|
+
execute: async (args, context) => {
|
|
936
|
+
try {
|
|
937
|
+
return JSON.stringify(await bff(context).getLandingPage(args));
|
|
938
|
+
}
|
|
939
|
+
catch (err) {
|
|
940
|
+
return readError(err, missingKeyMessage);
|
|
941
|
+
}
|
|
942
|
+
},
|
|
943
|
+
});
|
|
944
|
+
add({
|
|
945
|
+
name: 'unpublish_landing_page',
|
|
946
|
+
description: 'Take the hosted landing page offline (it answers 404). The public code and version history are ' +
|
|
947
|
+
'kept, so publishing again restores the same URL. A homepage registration that points at this ' +
|
|
948
|
+
`page is removed with it. Requires hosted.write. API reference: ${HOSTED_PAGES_DOCS}`,
|
|
949
|
+
parameters: z.object({ projectId: z.string().uuid() }),
|
|
950
|
+
execute: async (args, context) => {
|
|
951
|
+
try {
|
|
952
|
+
return JSON.stringify(await bff(context).unpublishLandingPage(args));
|
|
953
|
+
}
|
|
954
|
+
catch (err) {
|
|
955
|
+
return readError(err, missingKeyMessage);
|
|
956
|
+
}
|
|
957
|
+
},
|
|
958
|
+
});
|
|
723
959
|
add({
|
|
724
960
|
name: 'list_hosted_pages',
|
|
725
961
|
description: 'List the project registered hosted pages (homepage/support) with their verification status, content hash and last check time.',
|
|
@@ -736,7 +972,8 @@ export function registerTools(server, deps) {
|
|
|
736
972
|
});
|
|
737
973
|
add({
|
|
738
974
|
name: 'record_project_evidence',
|
|
739
|
-
description: 'Record test/scan/listing/privacy/payment evidence metadata bound to a browser, artifact hash and input hash. Source is local or agent
|
|
975
|
+
description: 'Record test/scan/listing/privacy/payment evidence metadata bound to a browser, artifact hash and input hash. Source is local or agent; CI evidence is written only by the verified ingest endpoint. Attachment files upload via the CLI (extenshi evidence push --attach); this tool records metadata only. Requires evidence.write OAuth scope.' +
|
|
976
|
+
PROVENANCE_HINT,
|
|
740
977
|
annotations: { readOnlyHint: false, idempotentHint: true, destructiveHint: false, openWorldHint: true },
|
|
741
978
|
parameters: agentEvidenceSchema,
|
|
742
979
|
execute: async (args, context) => {
|
|
@@ -751,10 +988,10 @@ export function registerTools(server, deps) {
|
|
|
751
988
|
add({
|
|
752
989
|
name: 'search_extensions',
|
|
753
990
|
description: 'Search the cross-store extension catalog (Chrome, Firefox, Edge) with hybrid relevance. ' +
|
|
754
|
-
'
|
|
755
|
-
'
|
|
756
|
-
'
|
|
757
|
-
'
|
|
991
|
+
'All filters (rating/reviews thresholds, store, category, pricing, risk, permissions, ' +
|
|
992
|
+
'freshness, manifest version, trader status) are applied server-side by the catalog ' +
|
|
993
|
+
'database, so a filtered request returns only the matching subset. `skip` pages through ' +
|
|
994
|
+
'large result sets. ' +
|
|
758
995
|
`${describeStoreConstraints()} ` +
|
|
759
996
|
'Returns a compact list for market research and competitive analysis.',
|
|
760
997
|
parameters: z.object({
|
|
@@ -762,7 +999,7 @@ export function registerTools(server, deps) {
|
|
|
762
999
|
stores: z
|
|
763
1000
|
.array(z.enum(['CHROME', 'FIREFOX', 'EDGE']))
|
|
764
1001
|
.optional()
|
|
765
|
-
.describe('
|
|
1002
|
+
.describe('Stores to search: CHROME, FIREFOX and/or EDGE.'),
|
|
766
1003
|
categories: z.array(z.string()).optional().describe('Catalog category slugs to include.'),
|
|
767
1004
|
pricing: z
|
|
768
1005
|
.array(z.enum(['FREE', 'FREEMIUM', 'IN_APP_PURCHASES', 'SUBSCRIPTION']))
|
|
@@ -779,9 +1016,9 @@ export function registerTools(server, deps) {
|
|
|
779
1016
|
.number()
|
|
780
1017
|
.min(0)
|
|
781
1018
|
.optional()
|
|
782
|
-
.describe('Minimum weekly downloads — FIREFOX-ONLY metric (Chrome/Edge
|
|
783
|
-
'
|
|
784
|
-
'Chrome/Edge-only `stores` filter
|
|
1019
|
+
.describe('Minimum weekly downloads — FIREFOX-ONLY metric (not reported by Chrome/Edge). ' +
|
|
1020
|
+
'Chrome/Edge popularity is available through sortBy:"popular". A request combining ' +
|
|
1021
|
+
'it with a Chrome/Edge-only `stores` filter is rejected.'),
|
|
785
1022
|
minReviews: z.number().min(0).optional().describe('Minimum number of store reviews/ratings.'),
|
|
786
1023
|
updatedWithin: z
|
|
787
1024
|
.enum(['30d', '90d', '1y', 'stale'])
|
|
@@ -806,7 +1043,7 @@ export function registerTools(server, deps) {
|
|
|
806
1043
|
includeDelisted: z
|
|
807
1044
|
.boolean()
|
|
808
1045
|
.optional()
|
|
809
|
-
.describe('
|
|
1046
|
+
.describe('Whether extensions whose store listing was removed are included (default: excluded).'),
|
|
810
1047
|
sortBy: z
|
|
811
1048
|
.enum(['relevance', 'popular', 'rating', 'recent', 'name', 'safety', 'trader', 'size'])
|
|
812
1049
|
.optional()
|
|
@@ -880,10 +1117,10 @@ export function registerTools(server, deps) {
|
|
|
880
1117
|
'reviews link). Chrome Web Store review rows are NOT returned (their text cannot be redistributed) — ' +
|
|
881
1118
|
'for a Chrome extension the `aggregate` (with a link to the store reviews tab) is the only public ' +
|
|
882
1119
|
'review content. Reviewer identity is intentionally omitted. ' +
|
|
883
|
-
'Paginated newest-first by default
|
|
884
|
-
'
|
|
885
|
-
'
|
|
886
|
-
'
|
|
1120
|
+
'Paginated newest-first by default (or by highest rating); `cursor` takes the `nextCursor` ' +
|
|
1121
|
+
'from a previous call. Cursors are sort-specific: a cursor is valid only with the `sort` ' +
|
|
1122
|
+
'it was issued for. Reads existing scraped reviews; `min_rating` narrows to positive or ' +
|
|
1123
|
+
'critical feedback.',
|
|
887
1124
|
parameters: z.object({
|
|
888
1125
|
...EXTENSION_REF_SHAPE,
|
|
889
1126
|
limit: z.number().int().min(1).max(50).default(20).describe('Max reviews to return (1–50).'),
|
|
@@ -891,7 +1128,7 @@ export function registerTools(server, deps) {
|
|
|
891
1128
|
.number()
|
|
892
1129
|
.int()
|
|
893
1130
|
.optional()
|
|
894
|
-
.describe('Pagination cursor
|
|
1131
|
+
.describe('Pagination cursor: the `nextCursor` value returned by a previous call.'),
|
|
895
1132
|
language_id: z.number().int().optional().describe('Only reviews in this catalog language id.'),
|
|
896
1133
|
min_rating: z
|
|
897
1134
|
.number()
|
|
@@ -930,9 +1167,8 @@ export function registerTools(server, deps) {
|
|
|
930
1167
|
description: 'Get the security analysis for an extension: safety score (0–100, higher = safer — the ' +
|
|
931
1168
|
'same coefficient the website shows), risk category, finding counts by ' +
|
|
932
1169
|
'severity, and the top grouped findings (scanner, rule, severity, count). Also returns ' +
|
|
933
|
-
'
|
|
934
|
-
'
|
|
935
|
-
'unscanned extensions. Reads existing scan results — does not trigger a new scan. ' +
|
|
1170
|
+
'an approximate install-dialog preview computed from the store-listed permissions ' +
|
|
1171
|
+
'(consolidated + deduped), available even for unscanned extensions. Reads existing scan results — does not trigger a new scan. ' +
|
|
936
1172
|
'Identify the extension by numeric `extension_id` OR `store_id` (+`store` for Chrome/Edge). ' +
|
|
937
1173
|
'NB: this tool costs 3 read credits (it fetches security, risk, and install-preview data).',
|
|
938
1174
|
parameters: z.object({ ...EXTENSION_REF_SHAPE }),
|
|
@@ -963,15 +1199,15 @@ export function registerTools(server, deps) {
|
|
|
963
1199
|
description: 'Look up the safety score and risk category for MANY extensions at once, addressed by their ' +
|
|
964
1200
|
'STORE ids (the id in the store URL) — the inventory case: you have a list of what a user has ' +
|
|
965
1201
|
`installed and need risk for all of it. Up to ${MAX_BATCH_EXTENSIONS} extensions per call, and ` +
|
|
966
|
-
'the WHOLE call costs 1 read credit (not 1 per extension
|
|
967
|
-
'
|
|
1202
|
+
'the WHOLE call costs 1 read credit (not 1 per extension; get_security costs 3 credits ' +
|
|
1203
|
+
'per extension). Returns safety score (0–100, higher ' +
|
|
968
1204
|
'= safer, the same number the website shows), risk category, severity counts, last scan date ' +
|
|
969
1205
|
'and the catalog URL per extension. Each store id is resolved through its cross-store cluster ' +
|
|
970
1206
|
'to the same listing the extension page renders, so these answers agree with the website. ' +
|
|
971
1207
|
'`store` is REQUIRED per entry — Chrome and Edge ids share one format. An extension with no ' +
|
|
972
1208
|
'score has not been scanned yet (`scanned: false`); ids with no catalog listing come back ' +
|
|
973
|
-
'under `notInCatalog`. Neither means "safe".
|
|
974
|
-
'get_security
|
|
1209
|
+
'under `notInCatalog`. Neither means "safe". The full findings list of ONE extension comes ' +
|
|
1210
|
+
'from get_security.',
|
|
975
1211
|
parameters: z.object({
|
|
976
1212
|
extensions: z
|
|
977
1213
|
.array(z.object({
|
|
@@ -986,8 +1222,8 @@ export function registerTools(server, deps) {
|
|
|
986
1222
|
}))
|
|
987
1223
|
.min(1)
|
|
988
1224
|
.max(MAX_BATCH_EXTENSIONS)
|
|
989
|
-
.describe(`The extensions to look up (max ${MAX_BATCH_EXTENSIONS}).
|
|
990
|
-
`
|
|
1225
|
+
.describe(`The extensions to look up (max ${MAX_BATCH_EXTENSIONS}). A longer list takes several ` +
|
|
1226
|
+
`calls of up to ${MAX_BATCH_EXTENSIONS}; each call costs 1 read credit.`),
|
|
991
1227
|
}),
|
|
992
1228
|
execute: async (args, context) => {
|
|
993
1229
|
try {
|
|
@@ -1011,10 +1247,22 @@ export function registerTools(server, deps) {
|
|
|
1011
1247
|
query: z.string().optional().describe('Optional query to scope facets to a search.'),
|
|
1012
1248
|
stores: z.array(z.enum(['CHROME', 'FIREFOX', 'EDGE'])).optional(),
|
|
1013
1249
|
categories: z.array(z.string()).optional(),
|
|
1250
|
+
facets: z
|
|
1251
|
+
.array(z.enum(MARKET_FACETS))
|
|
1252
|
+
.optional()
|
|
1253
|
+
.describe('Facet groups to include: stats, stores, categories, extended, questionnaire, downloads; all groups when omitted.'),
|
|
1254
|
+
top_n: z
|
|
1255
|
+
.number()
|
|
1256
|
+
.int()
|
|
1257
|
+
.min(1)
|
|
1258
|
+
.max(MAX_MARKET_TOP_N)
|
|
1259
|
+
.optional()
|
|
1260
|
+
.describe(`Entries kept per open-ended list (category tree levels, permissions, target sites), largest first. Default ${DEFAULT_MARKET_TOP_N}; \`truncated\` reports what was cut.`),
|
|
1014
1261
|
}),
|
|
1015
1262
|
execute: async (args, context) => {
|
|
1016
1263
|
try {
|
|
1017
1264
|
const client = bff(context);
|
|
1265
|
+
const shapeOpts = { facets: args.facets, topN: args.top_n };
|
|
1018
1266
|
const scoped = Boolean(args.query?.trim() || args.stores?.length || args.categories?.length);
|
|
1019
1267
|
// Search-scoped: progressive-narrowing facets over the match set.
|
|
1020
1268
|
if (scoped) {
|
|
@@ -1024,11 +1272,7 @@ export function registerTools(server, deps) {
|
|
|
1024
1272
|
.getSearchFacets({ query: args.query, stores: args.stores, categories: args.categories })
|
|
1025
1273
|
.catch(() => null),
|
|
1026
1274
|
]);
|
|
1027
|
-
return renderCatalogPayload({
|
|
1028
|
-
scope: 'search',
|
|
1029
|
-
stats: stats ?? undefined,
|
|
1030
|
-
facets: facets ?? undefined,
|
|
1031
|
-
});
|
|
1275
|
+
return renderCatalogPayload(shapeMarketOverview({ scope: 'search', stats: stats ?? undefined, facets: facets ?? undefined }, shapeOpts));
|
|
1032
1276
|
}
|
|
1033
1277
|
// Catalog-wide: the unscoped search facets are all-zero by design, so build the
|
|
1034
1278
|
// overview from the dedicated catalog-wide procedures (cached server-side).
|
|
@@ -1049,7 +1293,7 @@ export function registerTools(server, deps) {
|
|
|
1049
1293
|
count: r.count,
|
|
1050
1294
|
label: STORE_LABELS[r.store] ?? r.store,
|
|
1051
1295
|
}));
|
|
1052
|
-
return renderCatalogPayload({
|
|
1296
|
+
return renderCatalogPayload(shapeMarketOverview({
|
|
1053
1297
|
scope: 'catalog-wide',
|
|
1054
1298
|
stats: stats ?? undefined,
|
|
1055
1299
|
facets: {
|
|
@@ -1057,8 +1301,8 @@ export function registerTools(server, deps) {
|
|
|
1057
1301
|
categoryTree: categoryTree ?? undefined,
|
|
1058
1302
|
extended: extended ?? undefined,
|
|
1059
1303
|
},
|
|
1060
|
-
note: 'Monetization and download-volume facets are
|
|
1061
|
-
});
|
|
1304
|
+
note: 'Monetization and download-volume facets are computed only for a query-scoped overview.',
|
|
1305
|
+
}, shapeOpts));
|
|
1062
1306
|
}
|
|
1063
1307
|
catch (err) {
|
|
1064
1308
|
return readError(err, missingKeyMessage);
|
|
@@ -1067,15 +1311,15 @@ export function registerTools(server, deps) {
|
|
|
1067
1311
|
});
|
|
1068
1312
|
add({
|
|
1069
1313
|
name: 'get_credit_balance',
|
|
1070
|
-
description: '
|
|
1071
|
-
'
|
|
1314
|
+
description: 'Report your remaining Extenshi credits — for checking whether a large request fits the ' +
|
|
1315
|
+
'balance before running it. Returns every credit pool for your API key: `read` (spent one-per-call by ' +
|
|
1072
1316
|
'search_extensions / get_extension / get_reviews / market_overview / get_risk_by_store_ids — the ' +
|
|
1073
1317
|
'last one covers up to 40 extensions for that single credit; get_security costs 3) and ' +
|
|
1074
1318
|
'`scan` (spent by scan_extension), plus `icon` and `inventory`. Each pool reports `remaining` — the ' +
|
|
1075
1319
|
'number that actually gates calls (one-time free grant + purchased credits) — and `freeRemaining` ' +
|
|
1076
|
-
'(how much of the
|
|
1077
|
-
'spends
|
|
1078
|
-
'
|
|
1320
|
+
'(how much of the one-time, non-renewing signup grant is left). FREE to call: checking the balance ' +
|
|
1321
|
+
'spends no credit. Fetching N extensions takes read.remaining ≥ N (≥ 3N with get_security ' +
|
|
1322
|
+
'on each).',
|
|
1079
1323
|
parameters: z.object({}),
|
|
1080
1324
|
execute: async (_args, context) => {
|
|
1081
1325
|
try {
|
|
@@ -1098,9 +1342,9 @@ export function registerTools(server, deps) {
|
|
|
1098
1342
|
name: 'list_my_projects',
|
|
1099
1343
|
description: "List the extension projects owned by this API key — the developer's own workspaces on " +
|
|
1100
1344
|
'extenshi.io. Returns id, name, status, target browsers, the bound GitHub repo (if any) and ' +
|
|
1101
|
-
'the claimed store listing (if any).
|
|
1102
|
-
'"my project": the id
|
|
1103
|
-
'projects
|
|
1345
|
+
'the claimed store listing (if any). Applies when the developer refers to "my extension" or ' +
|
|
1346
|
+
'"my project": the returned id is what get_project_state takes. FREE — reading your own ' +
|
|
1347
|
+
'projects spends no credit.',
|
|
1104
1348
|
parameters: z.object({}),
|
|
1105
1349
|
execute: async (_args, context) => {
|
|
1106
1350
|
try {
|
|
@@ -1117,22 +1361,22 @@ export function registerTools(server, deps) {
|
|
|
1117
1361
|
'they picked (popup / side panel / page enhancer / in-page assistant), the required ' +
|
|
1118
1362
|
'permissions those types force, the exact manifest.json the project produces for each target ' +
|
|
1119
1363
|
'browser, the files that type needs, live hosted URLs (uninstall survey), and an index of ' +
|
|
1120
|
-
'every saved tool state with its size.
|
|
1121
|
-
'
|
|
1122
|
-
'
|
|
1123
|
-
'
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
'in it. `integration.unwired`
|
|
1127
|
-
'
|
|
1128
|
-
'row can be 256 KiB);
|
|
1129
|
-
'spends
|
|
1364
|
+
'every saved tool state with its size. It reflects the site configuration at the time of the ' +
|
|
1365
|
+
'call; later changes on the site appear in a later read. The additive workspace envelope ' +
|
|
1366
|
+
'holds revisioned local observations separately from these scaffold drafts. It also returns ' +
|
|
1367
|
+
'the integration contract: `integration.file` is the exact content for `integration.path` ' +
|
|
1368
|
+
'that wires the extension to this project. Those bytes carry a fingerprint extenshi.io uses ' +
|
|
1369
|
+
"to tell the developer's edits from its own; a reassembled equivalent file lacks it, and the " +
|
|
1370
|
+
'site then stops managing the values in it. `integration.unwired` lists the links this ' +
|
|
1371
|
+
'project has not set up yet (no URLs exist for them). Tool-state payloads are NOT inlined by ' +
|
|
1372
|
+
'default (a single row can be 256 KiB); includeToolStates selects which keys to inline. ' +
|
|
1373
|
+
'FREE — spends no credit. Type definitions are in list_extension_templates.',
|
|
1130
1374
|
parameters: z.object({
|
|
1131
1375
|
projectId: z.string().describe('Project id from list_my_projects.'),
|
|
1132
1376
|
includeToolStates: z
|
|
1133
1377
|
.array(z.string())
|
|
1134
1378
|
.optional()
|
|
1135
|
-
.describe('Tool keys whose saved payload to inline, e.g. ["manifest-generator", "privacy-policy-generator"].
|
|
1379
|
+
.describe('Tool keys whose saved payload to inline, e.g. ["manifest-generator", "privacy-policy-generator"]. When omitted, only the index is returned.'),
|
|
1136
1380
|
}),
|
|
1137
1381
|
execute: async (args, context) => {
|
|
1138
1382
|
try {
|
|
@@ -1152,18 +1396,18 @@ export function registerTools(server, deps) {
|
|
|
1152
1396
|
description: "The starter extension one of the developer's own projects produces, as FILES ready to " +
|
|
1153
1397
|
'write: manifest.json for the target browser, the background worker, the panel or ' +
|
|
1154
1398
|
"content-script files that project's types need, placeholder icons, and " +
|
|
1155
|
-
'src/extenshi.config.js.
|
|
1156
|
-
"
|
|
1157
|
-
'
|
|
1158
|
-
'
|
|
1159
|
-
"
|
|
1160
|
-
'FREE —
|
|
1399
|
+
'src/extenshi.config.js. The config carries a fingerprint that lets extenshi.io recognise ' +
|
|
1400
|
+
"the repository as this project's; a hand-rebuilt config lacks it, and the site then stops " +
|
|
1401
|
+
'managing those values. Intended for a NEW extension: it is a complete starter set whose ' +
|
|
1402
|
+
'files would replace same-named files in an existing repository. It is the same file set ' +
|
|
1403
|
+
"the site commits. One browser per call (default: the project's first target). " +
|
|
1404
|
+
'FREE — spends no credit.',
|
|
1161
1405
|
parameters: z.object({
|
|
1162
1406
|
projectId: z.string().describe('Project id from list_my_projects.'),
|
|
1163
1407
|
browser: z
|
|
1164
1408
|
.enum(['chrome', 'firefox', 'edge'])
|
|
1165
1409
|
.optional()
|
|
1166
|
-
.describe("Target browser
|
|
1410
|
+
.describe("Target browser; defaults to the project's first declared target."),
|
|
1167
1411
|
}),
|
|
1168
1412
|
execute: async (args, context) => {
|
|
1169
1413
|
try {
|
|
@@ -1198,8 +1442,8 @@ export function registerTools(server, deps) {
|
|
|
1198
1442
|
});
|
|
1199
1443
|
add({
|
|
1200
1444
|
name: 'get_privacy_policy_version',
|
|
1201
|
-
description: 'Read the markdown (and HTML) of one hosted privacy-policy version
|
|
1202
|
-
'
|
|
1445
|
+
description: 'Read the markdown (and HTML) of one hosted privacy-policy version; version numbers come from ' +
|
|
1446
|
+
'list_privacy_policy_versions. Pro only. Does not spend a credit.',
|
|
1203
1447
|
parameters: z.object({
|
|
1204
1448
|
projectId: z.string().describe('Project id from list_my_projects.'),
|
|
1205
1449
|
versionNumber: z
|
|
@@ -1219,17 +1463,17 @@ export function registerTools(server, deps) {
|
|
|
1219
1463
|
});
|
|
1220
1464
|
add({
|
|
1221
1465
|
name: 'publish_privacy_policy',
|
|
1222
|
-
description: 'Publish a hosted privacy policy for a Pro project.
|
|
1223
|
-
'the saved form and manifest (
|
|
1224
|
-
'
|
|
1225
|
-
'
|
|
1226
|
-
'
|
|
1466
|
+
description: 'Publish a hosted privacy policy for a Pro project. Changes the live public policy page. ' +
|
|
1467
|
+
'Without bodyMarkdown it generates the policy from the saved form and manifest (the first ' +
|
|
1468
|
+
'version is produced this way); with bodyMarkdown it publishes that edited (or AI-updated) ' +
|
|
1469
|
+
'draft at the same public URL. Once published, PRIVACY_POLICY_URL in the integration.file ' +
|
|
1470
|
+
'from get_project_state is the hosted URL. Pro only.',
|
|
1227
1471
|
parameters: z.object({
|
|
1228
1472
|
projectId: z.string().describe('Project id from list_my_projects.'),
|
|
1229
1473
|
bodyMarkdown: z
|
|
1230
1474
|
.string()
|
|
1231
1475
|
.optional()
|
|
1232
|
-
.describe('Full policy markdown.
|
|
1476
|
+
.describe('Full policy markdown. When omitted, the policy is generated from the saved form and manifest.'),
|
|
1233
1477
|
kind: z
|
|
1234
1478
|
.enum(['generated', 'edited', 'ai_updated', 'reverted'])
|
|
1235
1479
|
.optional()
|
|
@@ -1247,15 +1491,18 @@ export function registerTools(server, deps) {
|
|
|
1247
1491
|
add({
|
|
1248
1492
|
name: 'update_privacy_policy_with_ai',
|
|
1249
1493
|
description: "Propose an updated privacy policy that keeps the author's custom wording and adds sections " +
|
|
1250
|
-
'required by new permissions or data practices.
|
|
1251
|
-
'
|
|
1252
|
-
'
|
|
1494
|
+
'required by new permissions or data practices. Returns proposedMarkdown only; nothing goes ' +
|
|
1495
|
+
'live. aiStep states whether the AI step ran (ran | did_not_run | discarded | not_needed) and ' +
|
|
1496
|
+
'notice says why; unless aiStep is ran, proposedMarkdown is a deterministic section merge, not an ' +
|
|
1497
|
+
'AI draft. conflictHeadings and missingCategories list what still needs a manual edit. Publishing ' +
|
|
1498
|
+
'is a separate step (publish_privacy_policy with the returned kind). Requires a policy that is ' +
|
|
1499
|
+
'already published. Pro only.',
|
|
1253
1500
|
parameters: z.object({
|
|
1254
1501
|
projectId: z.string().describe('Project id from list_my_projects.'),
|
|
1255
1502
|
}),
|
|
1256
1503
|
execute: async (args, context) => {
|
|
1257
1504
|
try {
|
|
1258
|
-
return JSON.stringify(await bff(context).updatePrivacyPolicyWithAi(args), null, 2);
|
|
1505
|
+
return JSON.stringify(describePrivacyAiUpdate(await bff(context).updatePrivacyPolicyWithAi(args)), null, 2);
|
|
1259
1506
|
}
|
|
1260
1507
|
catch (err) {
|
|
1261
1508
|
return readError(err, missingKeyMessage);
|
|
@@ -1267,40 +1514,55 @@ export function registerTools(server, deps) {
|
|
|
1267
1514
|
if (caps.has('docs')) {
|
|
1268
1515
|
add({
|
|
1269
1516
|
name: 'get_development_guide',
|
|
1270
|
-
description: '
|
|
1517
|
+
description: 'For extension-development tasks: returns the complete tool inventory for THIS connection, ' +
|
|
1271
1518
|
'the Extenshi service directory with access requirements and docs links, GitHub/code placement ' +
|
|
1272
1519
|
'guidance, and the ordered workflow from idea and repository through implementation, assets, ' +
|
|
1273
1520
|
'privacy, tests/CI, store submission and maintenance. Distinguishes MCP tools from cabinet, ' +
|
|
1274
1521
|
'CLI and external steps. Free; no key, account lookup or network request.',
|
|
1275
|
-
parameters: z.object({
|
|
1276
|
-
|
|
1522
|
+
parameters: z.object({
|
|
1523
|
+
sections: z
|
|
1524
|
+
.array(z.enum(['all', ...GUIDE_SECTIONS]))
|
|
1525
|
+
.optional()
|
|
1526
|
+
.describe('Full guide sections to include: tools, localActions, services, repository, workflow, handoff, or all. ' +
|
|
1527
|
+
'Omitted: a compact overview with tool names, workflow stages and a table of contents.'),
|
|
1528
|
+
}),
|
|
1529
|
+
execute: async (args) => JSON.stringify(buildDevelopmentGuide(registeredTools, args?.sections), null, 2),
|
|
1277
1530
|
});
|
|
1278
1531
|
add({
|
|
1279
1532
|
name: 'search_docs',
|
|
1280
1533
|
description: 'Search the official Extenshi documentation (docs.extenshi.io) — product guides plus the ' +
|
|
1281
|
-
'full @extenshi/cli command reference (scan, review-risk, publish, login).
|
|
1282
|
-
'"how do I…" questions and to
|
|
1283
|
-
'
|
|
1284
|
-
'every available documentation page.',
|
|
1534
|
+
'full @extenshi/cli command reference (scan, review-risk, publish, login). Applies to ' +
|
|
1535
|
+
'"how do I…" questions and to exact CLI commands and flags. Free: reads public docs, no ' +
|
|
1536
|
+
'API key or quota required. Without a query it lists every available documentation page.',
|
|
1285
1537
|
parameters: z.object({
|
|
1286
1538
|
query: z
|
|
1287
1539
|
.string()
|
|
1288
1540
|
.optional()
|
|
1289
|
-
.describe('What to look up, e.g. "scan a zip in CI", "review-risk flags", "publish to edge", "get an API key".
|
|
1541
|
+
.describe('What to look up, e.g. "scan a zip in CI", "review-risk flags", "publish to edge", "get an API key". When omitted, every page is listed.'),
|
|
1290
1542
|
limit: z
|
|
1291
1543
|
.number()
|
|
1292
1544
|
.int()
|
|
1293
1545
|
.min(1)
|
|
1294
1546
|
.max(10)
|
|
1295
1547
|
.default(4)
|
|
1296
|
-
.describe('Max documentation
|
|
1548
|
+
.describe('Max documentation passages (page subsections) to return (1–10).'),
|
|
1549
|
+
max_chars: z
|
|
1550
|
+
.number()
|
|
1551
|
+
.int()
|
|
1552
|
+
.min(MIN_EXCERPT_CHARS)
|
|
1553
|
+
.max(MAX_EXCERPT_CHARS)
|
|
1554
|
+
.optional()
|
|
1555
|
+
.describe(`Max characters per passage excerpt (${MIN_EXCERPT_CHARS}–${MAX_EXCERPT_CHARS}, default ${DEFAULT_EXCERPT_CHARS}). Longer passages are cut with a link to the full page.`),
|
|
1297
1556
|
}),
|
|
1298
1557
|
execute: async (args) => {
|
|
1299
1558
|
try {
|
|
1300
1559
|
const query = args.query?.trim();
|
|
1301
1560
|
if (!query)
|
|
1302
1561
|
return await getDocsIndex(deps.cfg.docsUrl);
|
|
1303
|
-
return await searchDocs(deps.cfg.docsUrl, query,
|
|
1562
|
+
return await searchDocs(deps.cfg.docsUrl, query, {
|
|
1563
|
+
limit: args.limit ?? 4,
|
|
1564
|
+
maxChars: args.max_chars,
|
|
1565
|
+
});
|
|
1304
1566
|
}
|
|
1305
1567
|
catch (err) {
|
|
1306
1568
|
// Both branches keep the origin as `cause` — a docs outage is a
|
|
@@ -1341,11 +1603,11 @@ export function registerTools(server, deps) {
|
|
|
1341
1603
|
add({
|
|
1342
1604
|
name: 'generate_welcome_page_workflow',
|
|
1343
1605
|
description: 'Get the design brief for a browser-extension welcome page — the page a user lands on ' +
|
|
1344
|
-
'right after installing. Returns the one action the page
|
|
1606
|
+
'right after installing. Returns the one action the page is built to drive (pin the extension, ' +
|
|
1345
1607
|
'keep the new-tab change, use it on a site), exactly which illustrations to produce for ' +
|
|
1346
1608
|
'that goal, how to capture and crop them, how to place numbered/arrow markers showing ' +
|
|
1347
|
-
'where to click, the limits enforced on save, and the block JSON
|
|
1348
|
-
'
|
|
1609
|
+
'where to click, the limits enforced on save, and the block JSON format. Store screenshots ' +
|
|
1610
|
+
'Extenshi already hosts (from get_extension) can serve as illustrations. Static content: no ' +
|
|
1349
1611
|
'API key, no network, no credits.',
|
|
1350
1612
|
parameters: z.object({
|
|
1351
1613
|
extension_name: z.string().max(120).optional().describe('Extension display name.'),
|
|
@@ -1392,21 +1654,36 @@ export function registerTools(server, deps) {
|
|
|
1392
1654
|
accentColor: args.accent_color,
|
|
1393
1655
|
}),
|
|
1394
1656
|
});
|
|
1657
|
+
add({
|
|
1658
|
+
name: 'generate_landing_page',
|
|
1659
|
+
description: 'Generate a static landing page (homepage) for a browser extension as one self-contained HTML ' +
|
|
1660
|
+
'file: no JavaScript, escaped text, http(s)-only links (the support link may also be a mailto: ' +
|
|
1661
|
+
'address), and store buttons only for the store ' +
|
|
1662
|
+
'URLs given. Same generator as the cabinet Page generator, so a saved `page-generator` form ' +
|
|
1663
|
+
'from get_project_state maps field for field. Optional logo (inline SVG, validated and embedded ' +
|
|
1664
|
+
'as an image, or an image URL), screenshots and the canonical homepage URL for Open Graph and ' +
|
|
1665
|
+
'JSON-LD. Returns JSON {html, bytes, warnings, nextSteps}: warnings list input that was dropped ' +
|
|
1666
|
+
'or clamped; nextSteps cover hosting the file on HTTPS and registering the URL with ' +
|
|
1667
|
+
'upsert_hosted_page, which feeds HOMEPAGE_URL. Extenshi does not host the page. Static content: ' +
|
|
1668
|
+
'no API key, no network, no credits.',
|
|
1669
|
+
parameters: z.object(LANDING_FORM_SHAPE),
|
|
1670
|
+
execute: async (args) => JSON.stringify(renderGenerateLandingPage(args)),
|
|
1671
|
+
});
|
|
1395
1672
|
add({
|
|
1396
1673
|
name: 'list_extension_templates',
|
|
1397
1674
|
description: 'The kinds of browser extension you can build, with the permissions each one REQUIRES: ' +
|
|
1398
1675
|
'Popup (toolbar window), Side panel (docked beside the page), Page enhancer (runs on sites ' +
|
|
1399
1676
|
'you list), In-page assistant (your own UI over any site). Types combine, and the required ' +
|
|
1400
|
-
'permissions are the union.
|
|
1401
|
-
'
|
|
1402
|
-
'
|
|
1403
|
-
'
|
|
1404
|
-
"FREE: no API key, no quota.
|
|
1677
|
+
'permissions are the union. Applies when choosing manifest permissions: it gives the minimum ' +
|
|
1678
|
+
'each shape needs (store review flags padded permissions). Also states the cross-browser ' +
|
|
1679
|
+
'rules (Chromium side_panel vs Firefox sidebar_action) and the rule that a manifest may not ' +
|
|
1680
|
+
'name a file the package lacks. ' +
|
|
1681
|
+
"FREE: no API key, no quota. A specific project's chosen types are in get_project_state.",
|
|
1405
1682
|
parameters: z.object({
|
|
1406
1683
|
types: z
|
|
1407
1684
|
.array(z.string())
|
|
1408
1685
|
.optional()
|
|
1409
|
-
.describe('
|
|
1686
|
+
.describe('Type ids to include (popup, sidepanel, content, overlay); all four when omitted.'),
|
|
1410
1687
|
}),
|
|
1411
1688
|
execute: async (args) => renderExtensionTemplates(args.types),
|
|
1412
1689
|
});
|
|
@@ -1454,7 +1731,12 @@ export function registerTools(server, deps) {
|
|
|
1454
1731
|
}
|
|
1455
1732
|
},
|
|
1456
1733
|
});
|
|
1457
|
-
|
|
1734
|
+
// The scan report goes out as-is (defanged + labelled as data: finding
|
|
1735
|
+
// text quotes the artifact's own code). It used to be passed through
|
|
1736
|
+
// shapeExtension — the catalog-DETAIL shaper — whose field list has
|
|
1737
|
+
// none of the report's keys, so every scan came back as
|
|
1738
|
+
// {"snapshots": []} after the credit was spent.
|
|
1739
|
+
return renderCatalogPayload(report);
|
|
1458
1740
|
}
|
|
1459
1741
|
catch (err) {
|
|
1460
1742
|
// Keep the ScanError as `cause`: scanErrorMessage() renders 402/429
|
|
@@ -1470,14 +1752,15 @@ export function registerTools(server, deps) {
|
|
|
1470
1752
|
if (caps.has('publish')) {
|
|
1471
1753
|
add({
|
|
1472
1754
|
name: 'publish_extension',
|
|
1473
|
-
description: 'Publish an extension artifact (.zip/.crx/.xpi) to Chrome Web Store, Firefox AMO, and/or Edge Add-ons
|
|
1755
|
+
description: 'Publish an extension artifact (.zip/.crx/.xpi) to Chrome Web Store, Firefox AMO, and/or Edge Add-ons: ' +
|
|
1756
|
+
'uploads and submits a new version to each selected store. ' +
|
|
1474
1757
|
'FREE and fully local: the upload goes from this machine straight to the store APIs using store ' +
|
|
1475
1758
|
'credentials from the MCP server environment (CHROME_APP_ID/CHROME_CLIENT_ID/CHROME_CLIENT_SECRET/' +
|
|
1476
1759
|
'CHROME_REFRESH_TOKEN, FIREFOX_ADDON_GUID/FIREFOX_JWT_ISSUER/FIREFOX_JWT_SECRET, ' +
|
|
1477
1760
|
'EDGE_PRODUCT_ID/EDGE_CLIENT_ID/EDGE_CLIENT_SECRET/EDGE_TENANT_ID). The upload itself is local, but ' +
|
|
1478
|
-
'publishing is in an active testing phase: a quick Extenshi access check runs first (
|
|
1479
|
-
'
|
|
1480
|
-
'
|
|
1761
|
+
'publishing is in an active testing phase: a quick Extenshi access check runs first (it identifies your ' +
|
|
1762
|
+
'account by EXTENSHI_API_KEY). Edge submissions are polled to a terminal status. The artifact is ' +
|
|
1763
|
+
'not security-scanned by this tool (scanning is scan_extension).',
|
|
1481
1764
|
parameters: z.object({
|
|
1482
1765
|
artifact_path: z
|
|
1483
1766
|
.string()
|