@extenshi/mcp 0.7.0 → 0.8.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 +4 -3
- package/dist/bff.d.ts +5 -0
- package/dist/bff.d.ts.map +1 -1
- package/dist/bff.js +1 -0
- package/dist/bff.js.map +1 -1
- package/dist/development-guide.d.ts +108 -78
- package/dist/development-guide.d.ts.map +1 -1
- package/dist/development-guide.js +123 -44
- 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/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/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/templates.js +5 -5
- package/dist/templates.js.map +1 -1
- package/dist/tools.d.ts +1 -0
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +199 -111
- 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
|
@@ -35,8 +35,9 @@
|
|
|
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';
|
|
41
42
|
import { renderLocalizeWorkflow } from './localize-workflow.js';
|
|
42
43
|
import { PAY_OPERATIONS, paySchemas } from './pay.js';
|
|
@@ -44,7 +45,7 @@ import { PublishSetupError, publishArtifact, readStoreCredentials, validateStore
|
|
|
44
45
|
import { checkPublishAccess } from './publish-access.js';
|
|
45
46
|
import { ScanError, scanArtifact } from './scan.js';
|
|
46
47
|
import { describeStoreConstraints, validateSearchFilters } from './search-filters.js';
|
|
47
|
-
import { shapeExtension, shapeReviews, shapeSearch, shapeSecurity, shapeStoreRiskBatch } from './shape.js';
|
|
48
|
+
import { DEFAULT_MARKET_TOP_N, MARKET_FACETS, MAX_MARKET_TOP_N, shapeExtension, shapeMarketOverview, shapeReviews, shapeSearch, shapeSecurity, shapeStoreRiskBatch, } from './shape.js';
|
|
48
49
|
import { captureError, captureEvent, classifyError } from './telemetry.js';
|
|
49
50
|
import { renderExtensionTemplates } from './templates.js';
|
|
50
51
|
import { renderCatalogPayload } from './untrusted.js';
|
|
@@ -53,6 +54,10 @@ import { renderWelcomeWorkflow } from './welcome-workflow.js';
|
|
|
53
54
|
export const KEY_PAGE = 'https://dojo.extenshi.io/api-keys';
|
|
54
55
|
export const SIGNUP_PAGE = 'https://auth.extenshi.io/signup';
|
|
55
56
|
export const BILLING_PAGE = 'https://dojo.extenshi.io/billing';
|
|
57
|
+
export const HOSTED_PAGES_DOCS = 'https://docs.extenshi.io/developers/project-sync#hosted-pages-from-the-api';
|
|
58
|
+
// Every workspace write carries provenance; saying so up front saves the agent a
|
|
59
|
+
// failed first call on `provenance.toolVersion: Required`.
|
|
60
|
+
const PROVENANCE_HINT = ' `provenance` is required: {source: "agent", observedAt: ISO-8601 timestamp, toolVersion: your client name and version}.';
|
|
56
61
|
export const MISSING_KEY_MESSAGE = "This tool needs an Extenshi API key, and you don't have one set up yet — " +
|
|
57
62
|
"here's how to get going.\n\n" +
|
|
58
63
|
'Getting started is free: no credit card required. Every account includes a ' +
|
|
@@ -238,12 +243,34 @@ function attribution(session) {
|
|
|
238
243
|
* strongly typed as before.
|
|
239
244
|
* Fail-soft: telemetry never alters the tool's result or its thrown error.
|
|
240
245
|
*/
|
|
246
|
+
/**
|
|
247
|
+
* Publish every tool's input schema with all `$ref`s inlined.
|
|
248
|
+
*
|
|
249
|
+
* fastmcp converts `parameters` through xsschema, which runs zod-to-json-schema
|
|
250
|
+
* with its default `$refStrategy: 'root'`: every zod instance used twice in one
|
|
251
|
+
* schema (the shared `digest`, `provenanceSchema` strings, …) becomes a bare
|
|
252
|
+
* `{"$ref": "#/properties/…"}` with no `type`. The Claude connector directory
|
|
253
|
+
* rejects those ("Add a type to this parameter: inputDigest"), and weaker MCP
|
|
254
|
+
* clients cannot resolve them either. xsschema prefers a Standard JSON Schema
|
|
255
|
+
* `jsonSchema` hook when one is present, so we supply an inlined conversion
|
|
256
|
+
* there; validation still goes through the original zod `~standard.validate`.
|
|
257
|
+
*/
|
|
258
|
+
function withInlinedJsonSchema(parameters) {
|
|
259
|
+
const input = () => zodToJsonSchema(parameters, { $refStrategy: 'none' });
|
|
260
|
+
return Object.create(parameters, {
|
|
261
|
+
'~standard': { value: { ...parameters['~standard'], jsonSchema: { input, output: input } } },
|
|
262
|
+
});
|
|
263
|
+
}
|
|
241
264
|
function instrument(tool) {
|
|
242
265
|
const { name } = tool;
|
|
243
266
|
const original = tool.execute;
|
|
244
267
|
return {
|
|
245
268
|
...tool,
|
|
246
269
|
execute: async (args, context) => {
|
|
270
|
+
// The hosted connector lets a user decline analytics on the consent
|
|
271
|
+
// screen; honour it for every event, including error reports.
|
|
272
|
+
if (context.session?.telemetry === false)
|
|
273
|
+
return original(args, context);
|
|
247
274
|
const startedAt = Date.now();
|
|
248
275
|
const { userId, sessionId } = attribution(context.session);
|
|
249
276
|
const props = { tool: name, ...(sessionId ? { mcp_session_id: sessionId } : {}) };
|
|
@@ -290,8 +317,8 @@ const EXTENSION_REF_SHAPE = {
|
|
|
290
317
|
.max(100)
|
|
291
318
|
.optional()
|
|
292
319
|
.describe('Alternative to extension_id: the extension’s id in its store URL (e.g. Chrome ' +
|
|
293
|
-
'"cjpalhdlnbpafiamejdnhcphjbkeiagm").
|
|
294
|
-
'
|
|
320
|
+
'"cjpalhdlnbpafiamejdnhcphjbkeiagm"). A Chrome/Edge id (32 letters a–p) also requires ' +
|
|
321
|
+
'`store`: both stores share that format. Firefox ids (slug / GUID / email) are ' +
|
|
295
322
|
'unambiguous. Resolving a store_id is free; only the read that follows costs a credit.'),
|
|
296
323
|
store: z
|
|
297
324
|
.enum(['CHROME', 'FIREFOX', 'EDGE'])
|
|
@@ -511,6 +538,12 @@ const TOOL_ANNOTATIONS = {
|
|
|
511
538
|
idempotentHint: true,
|
|
512
539
|
openWorldHint: true,
|
|
513
540
|
},
|
|
541
|
+
remove_hosted_page: {
|
|
542
|
+
title: 'Remove a hosted page registration',
|
|
543
|
+
readOnlyHint: false,
|
|
544
|
+
destructiveHint: true,
|
|
545
|
+
openWorldHint: true,
|
|
546
|
+
},
|
|
514
547
|
};
|
|
515
548
|
/**
|
|
516
549
|
* Register the Extenshi tools once on a FastMCP server.
|
|
@@ -530,7 +563,8 @@ export function registerTools(server, deps) {
|
|
|
530
563
|
...tool.annotations,
|
|
531
564
|
title: tool.annotations?.title ?? TOOL_ANNOTATIONS[tool.name]?.title ?? tool.name.replaceAll('_', ' '),
|
|
532
565
|
};
|
|
533
|
-
|
|
566
|
+
const parameters = tool.parameters && withInlinedJsonSchema(tool.parameters);
|
|
567
|
+
server.addTool(instrument({ ...tool, annotations, ...(parameters ? { parameters } : {}) }));
|
|
534
568
|
registeredTools.push({ name: tool.name, description: tool.description ?? '', annotations });
|
|
535
569
|
}
|
|
536
570
|
// Per-call helpers bound to the injected deps.
|
|
@@ -545,7 +579,7 @@ export function registerTools(server, deps) {
|
|
|
545
579
|
for (const [method, name, , , mutation, description] of PAY_OPERATIONS) {
|
|
546
580
|
add({
|
|
547
581
|
name,
|
|
548
|
-
description: `${description} Requires ${mutation ? 'pay.write' : 'pay.read'} permission
|
|
582
|
+
description: `${description} Requires ${mutation ? 'pay.write' : 'pay.read'} permission.`,
|
|
549
583
|
parameters: paySchemas[method],
|
|
550
584
|
annotations: {
|
|
551
585
|
readOnlyHint: !mutation,
|
|
@@ -562,13 +596,22 @@ export function registerTools(server, deps) {
|
|
|
562
596
|
if (err instanceof UserError && /^MCP_SCOPE_REQUIRED:pay\.(read|write)\./.test(err.message))
|
|
563
597
|
throw err;
|
|
564
598
|
const status = err?.data?.httpStatus;
|
|
599
|
+
// Only OUR authored backend messages are matched, never echoed, so a
|
|
600
|
+
// provider string can't leak through; they pick the precise guidance.
|
|
601
|
+
const backendMessage = err instanceof Error ? err.message : '';
|
|
565
602
|
const message = status === 403
|
|
566
603
|
? 'Pay access denied. Grant pay.read/pay.write explicitly and verify application ownership.'
|
|
567
|
-
: status === 412
|
|
568
|
-
? 'Pay
|
|
569
|
-
: status ===
|
|
570
|
-
? 'Pay
|
|
571
|
-
:
|
|
604
|
+
: status === 412 && backendMessage.includes('Pay application is archived')
|
|
605
|
+
? 'This Pay application is archived. Use an active application from list_pay_apps, or create a new one.'
|
|
606
|
+
: status === 412
|
|
607
|
+
? 'Pay prerequisites are incomplete. Read get_pay_readiness; the author completes legal acceptance and Stripe KYC in the browser.'
|
|
608
|
+
: status === 404 && backendMessage.includes('No payment gateway connected')
|
|
609
|
+
? '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.'
|
|
610
|
+
: status === 404
|
|
611
|
+
? 'Pay application or resource not found. Check the appId with list_pay_apps.'
|
|
612
|
+
: status === 401
|
|
613
|
+
? 'Pay authentication failed. Reconnect or configure a developer API key with explicit Pay permissions.'
|
|
614
|
+
: 'Pay request failed. Check authentication, application readiness and backend support before retrying a mutation.';
|
|
572
615
|
// Provider failures may contain customer data or credentials; never echo them.
|
|
573
616
|
throw userErrorFrom(message, err);
|
|
574
617
|
}
|
|
@@ -577,7 +620,8 @@ export function registerTools(server, deps) {
|
|
|
577
620
|
}
|
|
578
621
|
add({
|
|
579
622
|
name: 'import_manifest',
|
|
580
|
-
description: 'Import an existing manifest.json into the Dojo manifest editor and project labels.
|
|
623
|
+
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.' +
|
|
624
|
+
PROVENANCE_HINT,
|
|
581
625
|
parameters: importManifestSchema,
|
|
582
626
|
annotations: { readOnlyHint: false, idempotentHint: true, destructiveHint: true, openWorldHint: true },
|
|
583
627
|
execute: async (args, context) => {
|
|
@@ -597,7 +641,10 @@ export function registerTools(server, deps) {
|
|
|
597
641
|
],
|
|
598
642
|
[
|
|
599
643
|
'get_release_readiness',
|
|
600
|
-
'Explain readiness for the recorded browser and artifact by locale. Local or agent reports
|
|
644
|
+
'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
|
+
'Readiness is measured against the release recorded in the workspace: until apply_project_patch sets patch.release ' +
|
|
646
|
+
'(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.',
|
|
601
648
|
'getReleaseReadiness',
|
|
602
649
|
],
|
|
603
650
|
];
|
|
@@ -629,7 +676,7 @@ export function registerTools(server, deps) {
|
|
|
629
676
|
});
|
|
630
677
|
add({
|
|
631
678
|
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;
|
|
679
|
+
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
680
|
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
|
|
634
681
|
parameters: z.object({}),
|
|
635
682
|
execute: async (_args, context) => {
|
|
@@ -653,8 +700,8 @@ export function registerTools(server, deps) {
|
|
|
653
700
|
add({
|
|
654
701
|
name,
|
|
655
702
|
description: dryRun
|
|
656
|
-
?
|
|
657
|
-
:
|
|
703
|
+
? `Preview a three-way metadata diff against a server-held base revision. No writes.${PROVENANCE_HINT}`
|
|
704
|
+
: `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
705
|
annotations: {
|
|
659
706
|
readOnlyHint: dryRun,
|
|
660
707
|
idempotentHint: true,
|
|
@@ -673,7 +720,7 @@ export function registerTools(server, deps) {
|
|
|
673
720
|
});
|
|
674
721
|
add({
|
|
675
722
|
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
|
|
723
|
+
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
724
|
annotations: { readOnlyHint: false, idempotentHint: false, destructiveHint: true, openWorldHint: true },
|
|
678
725
|
parameters: z.object({ projectId: z.string().uuid() }),
|
|
679
726
|
execute: async (args, context) => {
|
|
@@ -687,7 +734,7 @@ export function registerTools(server, deps) {
|
|
|
687
734
|
});
|
|
688
735
|
add({
|
|
689
736
|
name: 'upsert_hosted_page',
|
|
690
|
-
description:
|
|
737
|
+
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
738
|
annotations: { readOnlyHint: false, idempotentHint: true, destructiveHint: false, openWorldHint: true },
|
|
692
739
|
parameters: z.object({
|
|
693
740
|
projectId: z.string().uuid(),
|
|
@@ -705,7 +752,7 @@ export function registerTools(server, deps) {
|
|
|
705
752
|
});
|
|
706
753
|
add({
|
|
707
754
|
name: 'verify_hosted_artifact',
|
|
708
|
-
description:
|
|
755
|
+
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
756
|
annotations: { readOnlyHint: false, idempotentHint: true, destructiveHint: false, openWorldHint: true },
|
|
710
757
|
parameters: z.object({
|
|
711
758
|
projectId: z.string().uuid(),
|
|
@@ -720,6 +767,23 @@ export function registerTools(server, deps) {
|
|
|
720
767
|
}
|
|
721
768
|
},
|
|
722
769
|
});
|
|
770
|
+
add({
|
|
771
|
+
name: 'remove_hosted_page',
|
|
772
|
+
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}`,
|
|
773
|
+
annotations: { readOnlyHint: false, idempotentHint: true, destructiveHint: true, openWorldHint: true },
|
|
774
|
+
parameters: z.object({
|
|
775
|
+
projectId: z.string().uuid(),
|
|
776
|
+
kind: z.enum(['homepage', 'support']),
|
|
777
|
+
}),
|
|
778
|
+
execute: async (args, context) => {
|
|
779
|
+
try {
|
|
780
|
+
return JSON.stringify(await bff(context).removeHostedPage(args));
|
|
781
|
+
}
|
|
782
|
+
catch (err) {
|
|
783
|
+
return readError(err, missingKeyMessage);
|
|
784
|
+
}
|
|
785
|
+
},
|
|
786
|
+
});
|
|
723
787
|
add({
|
|
724
788
|
name: 'list_hosted_pages',
|
|
725
789
|
description: 'List the project registered hosted pages (homepage/support) with their verification status, content hash and last check time.',
|
|
@@ -736,7 +800,8 @@ export function registerTools(server, deps) {
|
|
|
736
800
|
});
|
|
737
801
|
add({
|
|
738
802
|
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
|
|
803
|
+
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.' +
|
|
804
|
+
PROVENANCE_HINT,
|
|
740
805
|
annotations: { readOnlyHint: false, idempotentHint: true, destructiveHint: false, openWorldHint: true },
|
|
741
806
|
parameters: agentEvidenceSchema,
|
|
742
807
|
execute: async (args, context) => {
|
|
@@ -751,10 +816,10 @@ export function registerTools(server, deps) {
|
|
|
751
816
|
add({
|
|
752
817
|
name: 'search_extensions',
|
|
753
818
|
description: 'Search the cross-store extension catalog (Chrome, Firefox, Edge) with hybrid relevance. ' +
|
|
754
|
-
'
|
|
755
|
-
'
|
|
756
|
-
'
|
|
757
|
-
'
|
|
819
|
+
'All filters (rating/reviews thresholds, store, category, pricing, risk, permissions, ' +
|
|
820
|
+
'freshness, manifest version, trader status) are applied server-side by the catalog ' +
|
|
821
|
+
'database, so a filtered request returns only the matching subset. `skip` pages through ' +
|
|
822
|
+
'large result sets. ' +
|
|
758
823
|
`${describeStoreConstraints()} ` +
|
|
759
824
|
'Returns a compact list for market research and competitive analysis.',
|
|
760
825
|
parameters: z.object({
|
|
@@ -762,7 +827,7 @@ export function registerTools(server, deps) {
|
|
|
762
827
|
stores: z
|
|
763
828
|
.array(z.enum(['CHROME', 'FIREFOX', 'EDGE']))
|
|
764
829
|
.optional()
|
|
765
|
-
.describe('
|
|
830
|
+
.describe('Stores to search: CHROME, FIREFOX and/or EDGE.'),
|
|
766
831
|
categories: z.array(z.string()).optional().describe('Catalog category slugs to include.'),
|
|
767
832
|
pricing: z
|
|
768
833
|
.array(z.enum(['FREE', 'FREEMIUM', 'IN_APP_PURCHASES', 'SUBSCRIPTION']))
|
|
@@ -779,9 +844,9 @@ export function registerTools(server, deps) {
|
|
|
779
844
|
.number()
|
|
780
845
|
.min(0)
|
|
781
846
|
.optional()
|
|
782
|
-
.describe('Minimum weekly downloads — FIREFOX-ONLY metric (Chrome/Edge
|
|
783
|
-
'
|
|
784
|
-
'Chrome/Edge-only `stores` filter
|
|
847
|
+
.describe('Minimum weekly downloads — FIREFOX-ONLY metric (not reported by Chrome/Edge). ' +
|
|
848
|
+
'Chrome/Edge popularity is available through sortBy:"popular". A request combining ' +
|
|
849
|
+
'it with a Chrome/Edge-only `stores` filter is rejected.'),
|
|
785
850
|
minReviews: z.number().min(0).optional().describe('Minimum number of store reviews/ratings.'),
|
|
786
851
|
updatedWithin: z
|
|
787
852
|
.enum(['30d', '90d', '1y', 'stale'])
|
|
@@ -806,7 +871,7 @@ export function registerTools(server, deps) {
|
|
|
806
871
|
includeDelisted: z
|
|
807
872
|
.boolean()
|
|
808
873
|
.optional()
|
|
809
|
-
.describe('
|
|
874
|
+
.describe('Whether extensions whose store listing was removed are included (default: excluded).'),
|
|
810
875
|
sortBy: z
|
|
811
876
|
.enum(['relevance', 'popular', 'rating', 'recent', 'name', 'safety', 'trader', 'size'])
|
|
812
877
|
.optional()
|
|
@@ -880,10 +945,10 @@ export function registerTools(server, deps) {
|
|
|
880
945
|
'reviews link). Chrome Web Store review rows are NOT returned (their text cannot be redistributed) — ' +
|
|
881
946
|
'for a Chrome extension the `aggregate` (with a link to the store reviews tab) is the only public ' +
|
|
882
947
|
'review content. Reviewer identity is intentionally omitted. ' +
|
|
883
|
-
'Paginated newest-first by default
|
|
884
|
-
'
|
|
885
|
-
'
|
|
886
|
-
'
|
|
948
|
+
'Paginated newest-first by default (or by highest rating); `cursor` takes the `nextCursor` ' +
|
|
949
|
+
'from a previous call. Cursors are sort-specific: a cursor is valid only with the `sort` ' +
|
|
950
|
+
'it was issued for. Reads existing scraped reviews; `min_rating` narrows to positive or ' +
|
|
951
|
+
'critical feedback.',
|
|
887
952
|
parameters: z.object({
|
|
888
953
|
...EXTENSION_REF_SHAPE,
|
|
889
954
|
limit: z.number().int().min(1).max(50).default(20).describe('Max reviews to return (1–50).'),
|
|
@@ -891,7 +956,7 @@ export function registerTools(server, deps) {
|
|
|
891
956
|
.number()
|
|
892
957
|
.int()
|
|
893
958
|
.optional()
|
|
894
|
-
.describe('Pagination cursor
|
|
959
|
+
.describe('Pagination cursor: the `nextCursor` value returned by a previous call.'),
|
|
895
960
|
language_id: z.number().int().optional().describe('Only reviews in this catalog language id.'),
|
|
896
961
|
min_rating: z
|
|
897
962
|
.number()
|
|
@@ -930,9 +995,8 @@ export function registerTools(server, deps) {
|
|
|
930
995
|
description: 'Get the security analysis for an extension: safety score (0–100, higher = safer — the ' +
|
|
931
996
|
'same coefficient the website shows), risk category, finding counts by ' +
|
|
932
997
|
'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. ' +
|
|
998
|
+
'an approximate install-dialog preview computed from the store-listed permissions ' +
|
|
999
|
+
'(consolidated + deduped), available even for unscanned extensions. Reads existing scan results — does not trigger a new scan. ' +
|
|
936
1000
|
'Identify the extension by numeric `extension_id` OR `store_id` (+`store` for Chrome/Edge). ' +
|
|
937
1001
|
'NB: this tool costs 3 read credits (it fetches security, risk, and install-preview data).',
|
|
938
1002
|
parameters: z.object({ ...EXTENSION_REF_SHAPE }),
|
|
@@ -963,15 +1027,15 @@ export function registerTools(server, deps) {
|
|
|
963
1027
|
description: 'Look up the safety score and risk category for MANY extensions at once, addressed by their ' +
|
|
964
1028
|
'STORE ids (the id in the store URL) — the inventory case: you have a list of what a user has ' +
|
|
965
1029
|
`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
|
-
'
|
|
1030
|
+
'the WHOLE call costs 1 read credit (not 1 per extension; get_security costs 3 credits ' +
|
|
1031
|
+
'per extension). Returns safety score (0–100, higher ' +
|
|
968
1032
|
'= safer, the same number the website shows), risk category, severity counts, last scan date ' +
|
|
969
1033
|
'and the catalog URL per extension. Each store id is resolved through its cross-store cluster ' +
|
|
970
1034
|
'to the same listing the extension page renders, so these answers agree with the website. ' +
|
|
971
1035
|
'`store` is REQUIRED per entry — Chrome and Edge ids share one format. An extension with no ' +
|
|
972
1036
|
'score has not been scanned yet (`scanned: false`); ids with no catalog listing come back ' +
|
|
973
|
-
'under `notInCatalog`. Neither means "safe".
|
|
974
|
-
'get_security
|
|
1037
|
+
'under `notInCatalog`. Neither means "safe". The full findings list of ONE extension comes ' +
|
|
1038
|
+
'from get_security.',
|
|
975
1039
|
parameters: z.object({
|
|
976
1040
|
extensions: z
|
|
977
1041
|
.array(z.object({
|
|
@@ -986,8 +1050,8 @@ export function registerTools(server, deps) {
|
|
|
986
1050
|
}))
|
|
987
1051
|
.min(1)
|
|
988
1052
|
.max(MAX_BATCH_EXTENSIONS)
|
|
989
|
-
.describe(`The extensions to look up (max ${MAX_BATCH_EXTENSIONS}).
|
|
990
|
-
`
|
|
1053
|
+
.describe(`The extensions to look up (max ${MAX_BATCH_EXTENSIONS}). A longer list takes several ` +
|
|
1054
|
+
`calls of up to ${MAX_BATCH_EXTENSIONS}; each call costs 1 read credit.`),
|
|
991
1055
|
}),
|
|
992
1056
|
execute: async (args, context) => {
|
|
993
1057
|
try {
|
|
@@ -1011,10 +1075,22 @@ export function registerTools(server, deps) {
|
|
|
1011
1075
|
query: z.string().optional().describe('Optional query to scope facets to a search.'),
|
|
1012
1076
|
stores: z.array(z.enum(['CHROME', 'FIREFOX', 'EDGE'])).optional(),
|
|
1013
1077
|
categories: z.array(z.string()).optional(),
|
|
1078
|
+
facets: z
|
|
1079
|
+
.array(z.enum(MARKET_FACETS))
|
|
1080
|
+
.optional()
|
|
1081
|
+
.describe('Facet groups to include: stats, stores, categories, extended, questionnaire, downloads; all groups when omitted.'),
|
|
1082
|
+
top_n: z
|
|
1083
|
+
.number()
|
|
1084
|
+
.int()
|
|
1085
|
+
.min(1)
|
|
1086
|
+
.max(MAX_MARKET_TOP_N)
|
|
1087
|
+
.optional()
|
|
1088
|
+
.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
1089
|
}),
|
|
1015
1090
|
execute: async (args, context) => {
|
|
1016
1091
|
try {
|
|
1017
1092
|
const client = bff(context);
|
|
1093
|
+
const shapeOpts = { facets: args.facets, topN: args.top_n };
|
|
1018
1094
|
const scoped = Boolean(args.query?.trim() || args.stores?.length || args.categories?.length);
|
|
1019
1095
|
// Search-scoped: progressive-narrowing facets over the match set.
|
|
1020
1096
|
if (scoped) {
|
|
@@ -1024,11 +1100,7 @@ export function registerTools(server, deps) {
|
|
|
1024
1100
|
.getSearchFacets({ query: args.query, stores: args.stores, categories: args.categories })
|
|
1025
1101
|
.catch(() => null),
|
|
1026
1102
|
]);
|
|
1027
|
-
return renderCatalogPayload({
|
|
1028
|
-
scope: 'search',
|
|
1029
|
-
stats: stats ?? undefined,
|
|
1030
|
-
facets: facets ?? undefined,
|
|
1031
|
-
});
|
|
1103
|
+
return renderCatalogPayload(shapeMarketOverview({ scope: 'search', stats: stats ?? undefined, facets: facets ?? undefined }, shapeOpts));
|
|
1032
1104
|
}
|
|
1033
1105
|
// Catalog-wide: the unscoped search facets are all-zero by design, so build the
|
|
1034
1106
|
// overview from the dedicated catalog-wide procedures (cached server-side).
|
|
@@ -1049,7 +1121,7 @@ export function registerTools(server, deps) {
|
|
|
1049
1121
|
count: r.count,
|
|
1050
1122
|
label: STORE_LABELS[r.store] ?? r.store,
|
|
1051
1123
|
}));
|
|
1052
|
-
return renderCatalogPayload({
|
|
1124
|
+
return renderCatalogPayload(shapeMarketOverview({
|
|
1053
1125
|
scope: 'catalog-wide',
|
|
1054
1126
|
stats: stats ?? undefined,
|
|
1055
1127
|
facets: {
|
|
@@ -1057,8 +1129,8 @@ export function registerTools(server, deps) {
|
|
|
1057
1129
|
categoryTree: categoryTree ?? undefined,
|
|
1058
1130
|
extended: extended ?? undefined,
|
|
1059
1131
|
},
|
|
1060
|
-
note: 'Monetization and download-volume facets are
|
|
1061
|
-
});
|
|
1132
|
+
note: 'Monetization and download-volume facets are computed only for a query-scoped overview.',
|
|
1133
|
+
}, shapeOpts));
|
|
1062
1134
|
}
|
|
1063
1135
|
catch (err) {
|
|
1064
1136
|
return readError(err, missingKeyMessage);
|
|
@@ -1067,15 +1139,15 @@ export function registerTools(server, deps) {
|
|
|
1067
1139
|
});
|
|
1068
1140
|
add({
|
|
1069
1141
|
name: 'get_credit_balance',
|
|
1070
|
-
description: '
|
|
1071
|
-
'
|
|
1142
|
+
description: 'Report your remaining Extenshi credits — for checking whether a large request fits the ' +
|
|
1143
|
+
'balance before running it. Returns every credit pool for your API key: `read` (spent one-per-call by ' +
|
|
1072
1144
|
'search_extensions / get_extension / get_reviews / market_overview / get_risk_by_store_ids — the ' +
|
|
1073
1145
|
'last one covers up to 40 extensions for that single credit; get_security costs 3) and ' +
|
|
1074
1146
|
'`scan` (spent by scan_extension), plus `icon` and `inventory`. Each pool reports `remaining` — the ' +
|
|
1075
1147
|
'number that actually gates calls (one-time free grant + purchased credits) — and `freeRemaining` ' +
|
|
1076
|
-
'(how much of the
|
|
1077
|
-
'spends
|
|
1078
|
-
'
|
|
1148
|
+
'(how much of the one-time, non-renewing signup grant is left). FREE to call: checking the balance ' +
|
|
1149
|
+
'spends no credit. Fetching N extensions takes read.remaining ≥ N (≥ 3N with get_security ' +
|
|
1150
|
+
'on each).',
|
|
1079
1151
|
parameters: z.object({}),
|
|
1080
1152
|
execute: async (_args, context) => {
|
|
1081
1153
|
try {
|
|
@@ -1098,9 +1170,9 @@ export function registerTools(server, deps) {
|
|
|
1098
1170
|
name: 'list_my_projects',
|
|
1099
1171
|
description: "List the extension projects owned by this API key — the developer's own workspaces on " +
|
|
1100
1172
|
'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
|
|
1173
|
+
'the claimed store listing (if any). Applies when the developer refers to "my extension" or ' +
|
|
1174
|
+
'"my project": the returned id is what get_project_state takes. FREE — reading your own ' +
|
|
1175
|
+
'projects spends no credit.',
|
|
1104
1176
|
parameters: z.object({}),
|
|
1105
1177
|
execute: async (_args, context) => {
|
|
1106
1178
|
try {
|
|
@@ -1117,22 +1189,22 @@ export function registerTools(server, deps) {
|
|
|
1117
1189
|
'they picked (popup / side panel / page enhancer / in-page assistant), the required ' +
|
|
1118
1190
|
'permissions those types force, the exact manifest.json the project produces for each target ' +
|
|
1119
1191
|
'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
|
|
1192
|
+
'every saved tool state with its size. It reflects the site configuration at the time of the ' +
|
|
1193
|
+
'call; later changes on the site appear in a later read. The additive workspace envelope ' +
|
|
1194
|
+
'holds revisioned local observations separately from these scaffold drafts. It also returns ' +
|
|
1195
|
+
'the integration contract: `integration.file` is the exact content for `integration.path` ' +
|
|
1196
|
+
'that wires the extension to this project. Those bytes carry a fingerprint extenshi.io uses ' +
|
|
1197
|
+
"to tell the developer's edits from its own; a reassembled equivalent file lacks it, and the " +
|
|
1198
|
+
'site then stops managing the values in it. `integration.unwired` lists the links this ' +
|
|
1199
|
+
'project has not set up yet (no URLs exist for them). Tool-state payloads are NOT inlined by ' +
|
|
1200
|
+
'default (a single row can be 256 KiB); includeToolStates selects which keys to inline. ' +
|
|
1201
|
+
'FREE — spends no credit. Type definitions are in list_extension_templates.',
|
|
1130
1202
|
parameters: z.object({
|
|
1131
1203
|
projectId: z.string().describe('Project id from list_my_projects.'),
|
|
1132
1204
|
includeToolStates: z
|
|
1133
1205
|
.array(z.string())
|
|
1134
1206
|
.optional()
|
|
1135
|
-
.describe('Tool keys whose saved payload to inline, e.g. ["manifest-generator", "privacy-policy-generator"].
|
|
1207
|
+
.describe('Tool keys whose saved payload to inline, e.g. ["manifest-generator", "privacy-policy-generator"]. When omitted, only the index is returned.'),
|
|
1136
1208
|
}),
|
|
1137
1209
|
execute: async (args, context) => {
|
|
1138
1210
|
try {
|
|
@@ -1152,18 +1224,18 @@ export function registerTools(server, deps) {
|
|
|
1152
1224
|
description: "The starter extension one of the developer's own projects produces, as FILES ready to " +
|
|
1153
1225
|
'write: manifest.json for the target browser, the background worker, the panel or ' +
|
|
1154
1226
|
"content-script files that project's types need, placeholder icons, and " +
|
|
1155
|
-
'src/extenshi.config.js.
|
|
1156
|
-
"
|
|
1157
|
-
'
|
|
1158
|
-
'
|
|
1159
|
-
"
|
|
1160
|
-
'FREE —
|
|
1227
|
+
'src/extenshi.config.js. The config carries a fingerprint that lets extenshi.io recognise ' +
|
|
1228
|
+
"the repository as this project's; a hand-rebuilt config lacks it, and the site then stops " +
|
|
1229
|
+
'managing those values. Intended for a NEW extension: it is a complete starter set whose ' +
|
|
1230
|
+
'files would replace same-named files in an existing repository. It is the same file set ' +
|
|
1231
|
+
"the site commits. One browser per call (default: the project's first target). " +
|
|
1232
|
+
'FREE — spends no credit.',
|
|
1161
1233
|
parameters: z.object({
|
|
1162
1234
|
projectId: z.string().describe('Project id from list_my_projects.'),
|
|
1163
1235
|
browser: z
|
|
1164
1236
|
.enum(['chrome', 'firefox', 'edge'])
|
|
1165
1237
|
.optional()
|
|
1166
|
-
.describe("Target browser
|
|
1238
|
+
.describe("Target browser; defaults to the project's first declared target."),
|
|
1167
1239
|
}),
|
|
1168
1240
|
execute: async (args, context) => {
|
|
1169
1241
|
try {
|
|
@@ -1198,8 +1270,8 @@ export function registerTools(server, deps) {
|
|
|
1198
1270
|
});
|
|
1199
1271
|
add({
|
|
1200
1272
|
name: 'get_privacy_policy_version',
|
|
1201
|
-
description: 'Read the markdown (and HTML) of one hosted privacy-policy version
|
|
1202
|
-
'
|
|
1273
|
+
description: 'Read the markdown (and HTML) of one hosted privacy-policy version; version numbers come from ' +
|
|
1274
|
+
'list_privacy_policy_versions. Pro only. Does not spend a credit.',
|
|
1203
1275
|
parameters: z.object({
|
|
1204
1276
|
projectId: z.string().describe('Project id from list_my_projects.'),
|
|
1205
1277
|
versionNumber: z
|
|
@@ -1219,17 +1291,17 @@ export function registerTools(server, deps) {
|
|
|
1219
1291
|
});
|
|
1220
1292
|
add({
|
|
1221
1293
|
name: 'publish_privacy_policy',
|
|
1222
|
-
description: 'Publish a hosted privacy policy for a Pro project.
|
|
1223
|
-
'the saved form and manifest (
|
|
1224
|
-
'
|
|
1225
|
-
'
|
|
1226
|
-
'
|
|
1294
|
+
description: 'Publish a hosted privacy policy for a Pro project. Changes the live public policy page. ' +
|
|
1295
|
+
'Without bodyMarkdown it generates the policy from the saved form and manifest (the first ' +
|
|
1296
|
+
'version is produced this way); with bodyMarkdown it publishes that edited (or AI-updated) ' +
|
|
1297
|
+
'draft at the same public URL. Once published, PRIVACY_POLICY_URL in the integration.file ' +
|
|
1298
|
+
'from get_project_state is the hosted URL. Pro only.',
|
|
1227
1299
|
parameters: z.object({
|
|
1228
1300
|
projectId: z.string().describe('Project id from list_my_projects.'),
|
|
1229
1301
|
bodyMarkdown: z
|
|
1230
1302
|
.string()
|
|
1231
1303
|
.optional()
|
|
1232
|
-
.describe('Full policy markdown.
|
|
1304
|
+
.describe('Full policy markdown. When omitted, the policy is generated from the saved form and manifest.'),
|
|
1233
1305
|
kind: z
|
|
1234
1306
|
.enum(['generated', 'edited', 'ai_updated', 'reverted'])
|
|
1235
1307
|
.optional()
|
|
@@ -1247,8 +1319,8 @@ export function registerTools(server, deps) {
|
|
|
1247
1319
|
add({
|
|
1248
1320
|
name: 'update_privacy_policy_with_ai',
|
|
1249
1321
|
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
|
-
'
|
|
1322
|
+
'required by new permissions or data practices. Returns proposedMarkdown only; nothing goes ' +
|
|
1323
|
+
'live. Publishing it is a separate step (publish_privacy_policy, kind ai_updated). Requires a ' +
|
|
1252
1324
|
'policy that is already published. Pro only.',
|
|
1253
1325
|
parameters: z.object({
|
|
1254
1326
|
projectId: z.string().describe('Project id from list_my_projects.'),
|
|
@@ -1267,40 +1339,55 @@ export function registerTools(server, deps) {
|
|
|
1267
1339
|
if (caps.has('docs')) {
|
|
1268
1340
|
add({
|
|
1269
1341
|
name: 'get_development_guide',
|
|
1270
|
-
description: '
|
|
1342
|
+
description: 'For extension-development tasks: returns the complete tool inventory for THIS connection, ' +
|
|
1271
1343
|
'the Extenshi service directory with access requirements and docs links, GitHub/code placement ' +
|
|
1272
1344
|
'guidance, and the ordered workflow from idea and repository through implementation, assets, ' +
|
|
1273
1345
|
'privacy, tests/CI, store submission and maintenance. Distinguishes MCP tools from cabinet, ' +
|
|
1274
1346
|
'CLI and external steps. Free; no key, account lookup or network request.',
|
|
1275
|
-
parameters: z.object({
|
|
1276
|
-
|
|
1347
|
+
parameters: z.object({
|
|
1348
|
+
sections: z
|
|
1349
|
+
.array(z.enum(['all', ...GUIDE_SECTIONS]))
|
|
1350
|
+
.optional()
|
|
1351
|
+
.describe('Full guide sections to include: tools, localActions, services, repository, workflow, handoff, or all. ' +
|
|
1352
|
+
'Omitted: a compact overview with tool names, workflow stages and a table of contents.'),
|
|
1353
|
+
}),
|
|
1354
|
+
execute: async (args) => JSON.stringify(buildDevelopmentGuide(registeredTools, args?.sections), null, 2),
|
|
1277
1355
|
});
|
|
1278
1356
|
add({
|
|
1279
1357
|
name: 'search_docs',
|
|
1280
1358
|
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.',
|
|
1359
|
+
'full @extenshi/cli command reference (scan, review-risk, publish, login). Applies to ' +
|
|
1360
|
+
'"how do I…" questions and to exact CLI commands and flags. Free: reads public docs, no ' +
|
|
1361
|
+
'API key or quota required. Without a query it lists every available documentation page.',
|
|
1285
1362
|
parameters: z.object({
|
|
1286
1363
|
query: z
|
|
1287
1364
|
.string()
|
|
1288
1365
|
.optional()
|
|
1289
|
-
.describe('What to look up, e.g. "scan a zip in CI", "review-risk flags", "publish to edge", "get an API key".
|
|
1366
|
+
.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
1367
|
limit: z
|
|
1291
1368
|
.number()
|
|
1292
1369
|
.int()
|
|
1293
1370
|
.min(1)
|
|
1294
1371
|
.max(10)
|
|
1295
1372
|
.default(4)
|
|
1296
|
-
.describe('Max documentation
|
|
1373
|
+
.describe('Max documentation passages (page subsections) to return (1–10).'),
|
|
1374
|
+
max_chars: z
|
|
1375
|
+
.number()
|
|
1376
|
+
.int()
|
|
1377
|
+
.min(MIN_EXCERPT_CHARS)
|
|
1378
|
+
.max(MAX_EXCERPT_CHARS)
|
|
1379
|
+
.optional()
|
|
1380
|
+
.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
1381
|
}),
|
|
1298
1382
|
execute: async (args) => {
|
|
1299
1383
|
try {
|
|
1300
1384
|
const query = args.query?.trim();
|
|
1301
1385
|
if (!query)
|
|
1302
1386
|
return await getDocsIndex(deps.cfg.docsUrl);
|
|
1303
|
-
return await searchDocs(deps.cfg.docsUrl, query,
|
|
1387
|
+
return await searchDocs(deps.cfg.docsUrl, query, {
|
|
1388
|
+
limit: args.limit ?? 4,
|
|
1389
|
+
maxChars: args.max_chars,
|
|
1390
|
+
});
|
|
1304
1391
|
}
|
|
1305
1392
|
catch (err) {
|
|
1306
1393
|
// Both branches keep the origin as `cause` — a docs outage is a
|
|
@@ -1341,11 +1428,11 @@ export function registerTools(server, deps) {
|
|
|
1341
1428
|
add({
|
|
1342
1429
|
name: 'generate_welcome_page_workflow',
|
|
1343
1430
|
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
|
|
1431
|
+
'right after installing. Returns the one action the page is built to drive (pin the extension, ' +
|
|
1345
1432
|
'keep the new-tab change, use it on a site), exactly which illustrations to produce for ' +
|
|
1346
1433
|
'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
|
-
'
|
|
1434
|
+
'where to click, the limits enforced on save, and the block JSON format. Store screenshots ' +
|
|
1435
|
+
'Extenshi already hosts (from get_extension) can serve as illustrations. Static content: no ' +
|
|
1349
1436
|
'API key, no network, no credits.',
|
|
1350
1437
|
parameters: z.object({
|
|
1351
1438
|
extension_name: z.string().max(120).optional().describe('Extension display name.'),
|
|
@@ -1397,16 +1484,16 @@ export function registerTools(server, deps) {
|
|
|
1397
1484
|
description: 'The kinds of browser extension you can build, with the permissions each one REQUIRES: ' +
|
|
1398
1485
|
'Popup (toolbar window), Side panel (docked beside the page), Page enhancer (runs on sites ' +
|
|
1399
1486
|
'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.
|
|
1487
|
+
'permissions are the union. Applies when choosing manifest permissions: it gives the minimum ' +
|
|
1488
|
+
'each shape needs (store review flags padded permissions). Also states the cross-browser ' +
|
|
1489
|
+
'rules (Chromium side_panel vs Firefox sidebar_action) and the rule that a manifest may not ' +
|
|
1490
|
+
'name a file the package lacks. ' +
|
|
1491
|
+
"FREE: no API key, no quota. A specific project's chosen types are in get_project_state.",
|
|
1405
1492
|
parameters: z.object({
|
|
1406
1493
|
types: z
|
|
1407
1494
|
.array(z.string())
|
|
1408
1495
|
.optional()
|
|
1409
|
-
.describe('
|
|
1496
|
+
.describe('Type ids to include (popup, sidepanel, content, overlay); all four when omitted.'),
|
|
1410
1497
|
}),
|
|
1411
1498
|
execute: async (args) => renderExtensionTemplates(args.types),
|
|
1412
1499
|
});
|
|
@@ -1470,14 +1557,15 @@ export function registerTools(server, deps) {
|
|
|
1470
1557
|
if (caps.has('publish')) {
|
|
1471
1558
|
add({
|
|
1472
1559
|
name: 'publish_extension',
|
|
1473
|
-
description: 'Publish an extension artifact (.zip/.crx/.xpi) to Chrome Web Store, Firefox AMO, and/or Edge Add-ons
|
|
1560
|
+
description: 'Publish an extension artifact (.zip/.crx/.xpi) to Chrome Web Store, Firefox AMO, and/or Edge Add-ons: ' +
|
|
1561
|
+
'uploads and submits a new version to each selected store. ' +
|
|
1474
1562
|
'FREE and fully local: the upload goes from this machine straight to the store APIs using store ' +
|
|
1475
1563
|
'credentials from the MCP server environment (CHROME_APP_ID/CHROME_CLIENT_ID/CHROME_CLIENT_SECRET/' +
|
|
1476
1564
|
'CHROME_REFRESH_TOKEN, FIREFOX_ADDON_GUID/FIREFOX_JWT_ISSUER/FIREFOX_JWT_SECRET, ' +
|
|
1477
1565
|
'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
|
-
'
|
|
1566
|
+
'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. The artifact is ' +
|
|
1568
|
+
'not security-scanned by this tool (scanning is scan_extension).',
|
|
1481
1569
|
parameters: z.object({
|
|
1482
1570
|
artifact_path: z
|
|
1483
1571
|
.string()
|