@extenshi/mcp 0.6.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 +35 -3
- package/dist/bff.d.ts +35 -5
- package/dist/bff.d.ts.map +1 -1
- package/dist/bff.js +15 -0
- package/dist/bff.js.map +1 -1
- package/dist/development-guide.d.ts +118 -76
- package/dist/development-guide.d.ts.map +1 -1
- package/dist/development-guide.js +151 -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 +8 -0
- package/dist/localize-workflow.d.ts.map +1 -0
- package/dist/localize-workflow.js +105 -0
- package/dist/localize-workflow.js.map +1 -0
- package/dist/pay.d.ts +214 -0
- package/dist/pay.d.ts.map +1 -0
- package/dist/pay.js +202 -0
- package/dist/pay.js.map +1 -0
- 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 +2 -1
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +333 -106
- 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 +10 -8
- package/dist/project-workspace.d.ts +0 -1976
- package/dist/project-workspace.d.ts.map +0 -1
- package/dist/project-workspace.js +0 -350
- package/dist/project-workspace.js.map +0 -1
package/dist/tools.js
CHANGED
|
@@ -26,23 +26,26 @@
|
|
|
26
26
|
* list_my_projects, get_project_state, get_project_scaffold,
|
|
27
27
|
* list/get/publish/update privacy policy (hosted; Pro)
|
|
28
28
|
* 'docs' → get_development_guide, search_docs, list_extension_templates, generate_icon_workflow,
|
|
29
|
-
* generate_welcome_page_workflow
|
|
29
|
+
* generate_welcome_page_workflow, localize_workflow (free; no key)
|
|
30
30
|
* 'scan' → scan_extension (local artifact; stdio only)
|
|
31
31
|
* 'publish' → publish_extension (local creds; stdio only)
|
|
32
32
|
*
|
|
33
33
|
* stdout is the MCP protocol channel for stdio — nothing here may write to it.
|
|
34
34
|
*/
|
|
35
|
+
import { agentEvidenceSchema, importManifestSchema, workspacePatchSchema } from '@extenshi/contracts';
|
|
35
36
|
import { UserError } from 'fastmcp';
|
|
36
37
|
import { z } from 'zod';
|
|
37
|
-
import {
|
|
38
|
-
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';
|
|
39
41
|
import { renderIconWorkflow } from './icon-workflow.js';
|
|
40
|
-
import {
|
|
42
|
+
import { renderLocalizeWorkflow } from './localize-workflow.js';
|
|
43
|
+
import { PAY_OPERATIONS, paySchemas } from './pay.js';
|
|
41
44
|
import { PublishSetupError, publishArtifact, readStoreCredentials, validateStoreCredentials, } from './publish.js';
|
|
42
45
|
import { checkPublishAccess } from './publish-access.js';
|
|
43
46
|
import { ScanError, scanArtifact } from './scan.js';
|
|
44
47
|
import { describeStoreConstraints, validateSearchFilters } from './search-filters.js';
|
|
45
|
-
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';
|
|
46
49
|
import { captureError, captureEvent, classifyError } from './telemetry.js';
|
|
47
50
|
import { renderExtensionTemplates } from './templates.js';
|
|
48
51
|
import { renderCatalogPayload } from './untrusted.js';
|
|
@@ -51,6 +54,10 @@ import { renderWelcomeWorkflow } from './welcome-workflow.js';
|
|
|
51
54
|
export const KEY_PAGE = 'https://dojo.extenshi.io/api-keys';
|
|
52
55
|
export const SIGNUP_PAGE = 'https://auth.extenshi.io/signup';
|
|
53
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}.';
|
|
54
61
|
export const MISSING_KEY_MESSAGE = "This tool needs an Extenshi API key, and you don't have one set up yet — " +
|
|
55
62
|
"here's how to get going.\n\n" +
|
|
56
63
|
'Getting started is free: no credit card required. Every account includes a ' +
|
|
@@ -78,7 +85,7 @@ export const SERVER_INSTRUCTIONS = 'Extenshi helps develop and operate browser e
|
|
|
78
85
|
'get_development_guide (free): it lists every tool on this connection, the service directory, ' +
|
|
79
86
|
'account/local prerequisites, documentation links, GitHub repository guidance and the ordered ' +
|
|
80
87
|
'plan from scope and research through code, assets, privacy, CI, store release and maintenance. ' +
|
|
81
|
-
'Keep a checklist covering the whole requested lifecycle. Use search_docs (free) for current ' +
|
|
88
|
+
'Keep a checklist covering the whole requested lifecycle. For standalone Pay, start with list_pay_apps; create_pay_app needs no development project or repository. Request explicit pay.read/pay.write scopes, use get_pay_readiness, and leave agreement acceptance and KYC to the author in the browser. Only the publishable SDK key and public verification key belong in extension code. Use search_docs (free) for current ' +
|
|
82
89
|
'product details and exact CLI flags. With identity, call list_my_projects then get_project_state ' +
|
|
83
90
|
'to reuse the existing project and repository. Use list_extension_templates before the manifest, ' +
|
|
84
91
|
'and get_project_scaffold for a new project. Preserve existing source. For an existing extension, read get_project_workspace then preview and apply import_manifest to fill Dojo from its real manifest and default-locale messages before configuring services. Review the integration diff and preserve local edits; write integration.file ' +
|
|
@@ -236,12 +243,34 @@ function attribution(session) {
|
|
|
236
243
|
* strongly typed as before.
|
|
237
244
|
* Fail-soft: telemetry never alters the tool's result or its thrown error.
|
|
238
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
|
+
}
|
|
239
264
|
function instrument(tool) {
|
|
240
265
|
const { name } = tool;
|
|
241
266
|
const original = tool.execute;
|
|
242
267
|
return {
|
|
243
268
|
...tool,
|
|
244
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);
|
|
245
274
|
const startedAt = Date.now();
|
|
246
275
|
const { userId, sessionId } = attribution(context.session);
|
|
247
276
|
const props = { tool: name, ...(sessionId ? { mcp_session_id: sessionId } : {}) };
|
|
@@ -288,8 +317,8 @@ const EXTENSION_REF_SHAPE = {
|
|
|
288
317
|
.max(100)
|
|
289
318
|
.optional()
|
|
290
319
|
.describe('Alternative to extension_id: the extension’s id in its store URL (e.g. Chrome ' +
|
|
291
|
-
'"cjpalhdlnbpafiamejdnhcphjbkeiagm").
|
|
292
|
-
'
|
|
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 ' +
|
|
293
322
|
'unambiguous. Resolving a store_id is free; only the read that follows costs a credit.'),
|
|
294
323
|
store: z
|
|
295
324
|
.enum(['CHROME', 'FIREFOX', 'EDGE'])
|
|
@@ -454,6 +483,12 @@ const TOOL_ANNOTATIONS = {
|
|
|
454
483
|
destructiveHint: false,
|
|
455
484
|
openWorldHint: true,
|
|
456
485
|
},
|
|
486
|
+
localize_workflow: {
|
|
487
|
+
title: 'Local extension localization workflow',
|
|
488
|
+
readOnlyHint: true,
|
|
489
|
+
idempotentHint: true,
|
|
490
|
+
openWorldHint: false,
|
|
491
|
+
},
|
|
457
492
|
generate_icon_workflow: {
|
|
458
493
|
title: 'Icon design workflow guide',
|
|
459
494
|
readOnlyHint: true,
|
|
@@ -478,6 +513,37 @@ const TOOL_ANNOTATIONS = {
|
|
|
478
513
|
destructiveHint: true,
|
|
479
514
|
openWorldHint: true,
|
|
480
515
|
},
|
|
516
|
+
create_ci_ingest_secret: {
|
|
517
|
+
title: 'Create or rotate the CI evidence ingest secret',
|
|
518
|
+
readOnlyHint: false,
|
|
519
|
+
destructiveHint: true,
|
|
520
|
+
openWorldHint: true,
|
|
521
|
+
},
|
|
522
|
+
upsert_hosted_page: {
|
|
523
|
+
title: 'Register a hosted page URL',
|
|
524
|
+
readOnlyHint: false,
|
|
525
|
+
destructiveHint: false,
|
|
526
|
+
openWorldHint: true,
|
|
527
|
+
},
|
|
528
|
+
verify_hosted_artifact: {
|
|
529
|
+
title: 'Verify a hosted page matches its record',
|
|
530
|
+
readOnlyHint: false,
|
|
531
|
+
idempotentHint: true,
|
|
532
|
+
destructiveHint: false,
|
|
533
|
+
openWorldHint: true,
|
|
534
|
+
},
|
|
535
|
+
list_hosted_pages: {
|
|
536
|
+
title: 'List hosted page registrations',
|
|
537
|
+
readOnlyHint: true,
|
|
538
|
+
idempotentHint: true,
|
|
539
|
+
openWorldHint: true,
|
|
540
|
+
},
|
|
541
|
+
remove_hosted_page: {
|
|
542
|
+
title: 'Remove a hosted page registration',
|
|
543
|
+
readOnlyHint: false,
|
|
544
|
+
destructiveHint: true,
|
|
545
|
+
openWorldHint: true,
|
|
546
|
+
},
|
|
481
547
|
};
|
|
482
548
|
/**
|
|
483
549
|
* Register the Extenshi tools once on a FastMCP server.
|
|
@@ -497,7 +563,8 @@ export function registerTools(server, deps) {
|
|
|
497
563
|
...tool.annotations,
|
|
498
564
|
title: tool.annotations?.title ?? TOOL_ANNOTATIONS[tool.name]?.title ?? tool.name.replaceAll('_', ' '),
|
|
499
565
|
};
|
|
500
|
-
|
|
566
|
+
const parameters = tool.parameters && withInlinedJsonSchema(tool.parameters);
|
|
567
|
+
server.addTool(instrument({ ...tool, annotations, ...(parameters ? { parameters } : {}) }));
|
|
501
568
|
registeredTools.push({ name: tool.name, description: tool.description ?? '', annotations });
|
|
502
569
|
}
|
|
503
570
|
// Per-call helpers bound to the injected deps.
|
|
@@ -509,9 +576,52 @@ export function registerTools(server, deps) {
|
|
|
509
576
|
};
|
|
510
577
|
// ── Read tools (free; key/identity required) ───────────────────────────────
|
|
511
578
|
if (caps.has('read')) {
|
|
579
|
+
for (const [method, name, , , mutation, description] of PAY_OPERATIONS) {
|
|
580
|
+
add({
|
|
581
|
+
name,
|
|
582
|
+
description: `${description} Requires ${mutation ? 'pay.write' : 'pay.read'} permission.`,
|
|
583
|
+
parameters: paySchemas[method],
|
|
584
|
+
annotations: {
|
|
585
|
+
readOnlyHint: !mutation,
|
|
586
|
+
destructiveHint: mutation,
|
|
587
|
+
idempotentHint: !['createPayApp', 'connectPaySeller', 'rotatePayKey'].includes(method),
|
|
588
|
+
openWorldHint: true,
|
|
589
|
+
},
|
|
590
|
+
execute: async (args, context) => {
|
|
591
|
+
try {
|
|
592
|
+
const invoke = bff(context)[method];
|
|
593
|
+
return JSON.stringify(await invoke(args));
|
|
594
|
+
}
|
|
595
|
+
catch (err) {
|
|
596
|
+
if (err instanceof UserError && /^MCP_SCOPE_REQUIRED:pay\.(read|write)\./.test(err.message))
|
|
597
|
+
throw err;
|
|
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 : '';
|
|
602
|
+
const message = status === 403
|
|
603
|
+
? 'Pay access denied. Grant pay.read/pay.write explicitly and verify application ownership.'
|
|
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.';
|
|
615
|
+
// Provider failures may contain customer data or credentials; never echo them.
|
|
616
|
+
throw userErrorFrom(message, err);
|
|
617
|
+
}
|
|
618
|
+
},
|
|
619
|
+
});
|
|
620
|
+
}
|
|
512
621
|
add({
|
|
513
622
|
name: 'import_manifest',
|
|
514
|
-
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,
|
|
515
625
|
parameters: importManifestSchema,
|
|
516
626
|
annotations: { readOnlyHint: false, idempotentHint: true, destructiveHint: true, openWorldHint: true },
|
|
517
627
|
execute: async (args, context) => {
|
|
@@ -531,7 +641,10 @@ export function registerTools(server, deps) {
|
|
|
531
641
|
],
|
|
532
642
|
[
|
|
533
643
|
'get_release_readiness',
|
|
534
|
-
'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.',
|
|
535
648
|
'getReleaseReadiness',
|
|
536
649
|
],
|
|
537
650
|
];
|
|
@@ -563,7 +676,7 @@ export function registerTools(server, deps) {
|
|
|
563
676
|
});
|
|
564
677
|
add({
|
|
565
678
|
name: 'connection_diagnostics',
|
|
566
|
-
description: 'Verify authentication, granted scopes, supported workspace contracts and actual write permissions. 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.',
|
|
567
680
|
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
|
|
568
681
|
parameters: z.object({}),
|
|
569
682
|
execute: async (_args, context) => {
|
|
@@ -587,8 +700,8 @@ export function registerTools(server, deps) {
|
|
|
587
700
|
add({
|
|
588
701
|
name,
|
|
589
702
|
description: dryRun
|
|
590
|
-
?
|
|
591
|
-
:
|
|
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}`,
|
|
592
705
|
annotations: {
|
|
593
706
|
readOnlyHint: dryRun,
|
|
594
707
|
idempotentHint: true,
|
|
@@ -605,11 +718,92 @@ export function registerTools(server, deps) {
|
|
|
605
718
|
}
|
|
606
719
|
},
|
|
607
720
|
});
|
|
721
|
+
add({
|
|
722
|
+
name: 'create_ci_ingest_secret',
|
|
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.',
|
|
724
|
+
annotations: { readOnlyHint: false, idempotentHint: false, destructiveHint: true, openWorldHint: true },
|
|
725
|
+
parameters: z.object({ projectId: z.string().uuid() }),
|
|
726
|
+
execute: async (args, context) => {
|
|
727
|
+
try {
|
|
728
|
+
return JSON.stringify(await bff(context).createCiIngestSecret(args));
|
|
729
|
+
}
|
|
730
|
+
catch (err) {
|
|
731
|
+
return readError(err, missingKeyMessage);
|
|
732
|
+
}
|
|
733
|
+
},
|
|
734
|
+
});
|
|
735
|
+
add({
|
|
736
|
+
name: 'upsert_hosted_page',
|
|
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}`,
|
|
738
|
+
annotations: { readOnlyHint: false, idempotentHint: true, destructiveHint: false, openWorldHint: true },
|
|
739
|
+
parameters: z.object({
|
|
740
|
+
projectId: z.string().uuid(),
|
|
741
|
+
kind: z.enum(['homepage', 'support']),
|
|
742
|
+
url: z.string().url().max(2000),
|
|
743
|
+
}),
|
|
744
|
+
execute: async (args, context) => {
|
|
745
|
+
try {
|
|
746
|
+
return JSON.stringify(await bff(context).upsertHostedPage(args));
|
|
747
|
+
}
|
|
748
|
+
catch (err) {
|
|
749
|
+
return readError(err, missingKeyMessage);
|
|
750
|
+
}
|
|
751
|
+
},
|
|
752
|
+
});
|
|
753
|
+
add({
|
|
754
|
+
name: 'verify_hosted_artifact',
|
|
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}`,
|
|
756
|
+
annotations: { readOnlyHint: false, idempotentHint: true, destructiveHint: false, openWorldHint: true },
|
|
757
|
+
parameters: z.object({
|
|
758
|
+
projectId: z.string().uuid(),
|
|
759
|
+
kind: z.enum(['homepage', 'support', 'privacy']),
|
|
760
|
+
}),
|
|
761
|
+
execute: async (args, context) => {
|
|
762
|
+
try {
|
|
763
|
+
return JSON.stringify(await bff(context).verifyHostedArtifact(args));
|
|
764
|
+
}
|
|
765
|
+
catch (err) {
|
|
766
|
+
return readError(err, missingKeyMessage);
|
|
767
|
+
}
|
|
768
|
+
},
|
|
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
|
+
});
|
|
787
|
+
add({
|
|
788
|
+
name: 'list_hosted_pages',
|
|
789
|
+
description: 'List the project registered hosted pages (homepage/support) with their verification status, content hash and last check time.',
|
|
790
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
|
|
791
|
+
parameters: z.object({ projectId: z.string().uuid() }),
|
|
792
|
+
execute: async (args, context) => {
|
|
793
|
+
try {
|
|
794
|
+
return JSON.stringify(await bff(context).listHostedPages(args));
|
|
795
|
+
}
|
|
796
|
+
catch (err) {
|
|
797
|
+
return readError(err, missingKeyMessage);
|
|
798
|
+
}
|
|
799
|
+
},
|
|
800
|
+
});
|
|
608
801
|
add({
|
|
609
802
|
name: 'record_project_evidence',
|
|
610
|
-
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,
|
|
611
805
|
annotations: { readOnlyHint: false, idempotentHint: true, destructiveHint: false, openWorldHint: true },
|
|
612
|
-
parameters:
|
|
806
|
+
parameters: agentEvidenceSchema,
|
|
613
807
|
execute: async (args, context) => {
|
|
614
808
|
try {
|
|
615
809
|
return JSON.stringify(await bff(context).recordProjectEvidence(args));
|
|
@@ -622,10 +816,10 @@ export function registerTools(server, deps) {
|
|
|
622
816
|
add({
|
|
623
817
|
name: 'search_extensions',
|
|
624
818
|
description: 'Search the cross-store extension catalog (Chrome, Firefox, Edge) with hybrid relevance. ' +
|
|
625
|
-
'
|
|
626
|
-
'
|
|
627
|
-
'
|
|
628
|
-
'
|
|
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. ' +
|
|
629
823
|
`${describeStoreConstraints()} ` +
|
|
630
824
|
'Returns a compact list for market research and competitive analysis.',
|
|
631
825
|
parameters: z.object({
|
|
@@ -633,7 +827,7 @@ export function registerTools(server, deps) {
|
|
|
633
827
|
stores: z
|
|
634
828
|
.array(z.enum(['CHROME', 'FIREFOX', 'EDGE']))
|
|
635
829
|
.optional()
|
|
636
|
-
.describe('
|
|
830
|
+
.describe('Stores to search: CHROME, FIREFOX and/or EDGE.'),
|
|
637
831
|
categories: z.array(z.string()).optional().describe('Catalog category slugs to include.'),
|
|
638
832
|
pricing: z
|
|
639
833
|
.array(z.enum(['FREE', 'FREEMIUM', 'IN_APP_PURCHASES', 'SUBSCRIPTION']))
|
|
@@ -650,9 +844,9 @@ export function registerTools(server, deps) {
|
|
|
650
844
|
.number()
|
|
651
845
|
.min(0)
|
|
652
846
|
.optional()
|
|
653
|
-
.describe('Minimum weekly downloads — FIREFOX-ONLY metric (Chrome/Edge
|
|
654
|
-
'
|
|
655
|
-
'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.'),
|
|
656
850
|
minReviews: z.number().min(0).optional().describe('Minimum number of store reviews/ratings.'),
|
|
657
851
|
updatedWithin: z
|
|
658
852
|
.enum(['30d', '90d', '1y', 'stale'])
|
|
@@ -677,7 +871,7 @@ export function registerTools(server, deps) {
|
|
|
677
871
|
includeDelisted: z
|
|
678
872
|
.boolean()
|
|
679
873
|
.optional()
|
|
680
|
-
.describe('
|
|
874
|
+
.describe('Whether extensions whose store listing was removed are included (default: excluded).'),
|
|
681
875
|
sortBy: z
|
|
682
876
|
.enum(['relevance', 'popular', 'rating', 'recent', 'name', 'safety', 'trader', 'size'])
|
|
683
877
|
.optional()
|
|
@@ -751,10 +945,10 @@ export function registerTools(server, deps) {
|
|
|
751
945
|
'reviews link). Chrome Web Store review rows are NOT returned (their text cannot be redistributed) — ' +
|
|
752
946
|
'for a Chrome extension the `aggregate` (with a link to the store reviews tab) is the only public ' +
|
|
753
947
|
'review content. Reviewer identity is intentionally omitted. ' +
|
|
754
|
-
'Paginated newest-first by default
|
|
755
|
-
'
|
|
756
|
-
'
|
|
757
|
-
'
|
|
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.',
|
|
758
952
|
parameters: z.object({
|
|
759
953
|
...EXTENSION_REF_SHAPE,
|
|
760
954
|
limit: z.number().int().min(1).max(50).default(20).describe('Max reviews to return (1–50).'),
|
|
@@ -762,7 +956,7 @@ export function registerTools(server, deps) {
|
|
|
762
956
|
.number()
|
|
763
957
|
.int()
|
|
764
958
|
.optional()
|
|
765
|
-
.describe('Pagination cursor
|
|
959
|
+
.describe('Pagination cursor: the `nextCursor` value returned by a previous call.'),
|
|
766
960
|
language_id: z.number().int().optional().describe('Only reviews in this catalog language id.'),
|
|
767
961
|
min_rating: z
|
|
768
962
|
.number()
|
|
@@ -801,9 +995,8 @@ export function registerTools(server, deps) {
|
|
|
801
995
|
description: 'Get the security analysis for an extension: safety score (0–100, higher = safer — the ' +
|
|
802
996
|
'same coefficient the website shows), risk category, finding counts by ' +
|
|
803
997
|
'severity, and the top grouped findings (scanner, rule, severity, count). Also returns ' +
|
|
804
|
-
'
|
|
805
|
-
'
|
|
806
|
-
'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. ' +
|
|
807
1000
|
'Identify the extension by numeric `extension_id` OR `store_id` (+`store` for Chrome/Edge). ' +
|
|
808
1001
|
'NB: this tool costs 3 read credits (it fetches security, risk, and install-preview data).',
|
|
809
1002
|
parameters: z.object({ ...EXTENSION_REF_SHAPE }),
|
|
@@ -834,15 +1027,15 @@ export function registerTools(server, deps) {
|
|
|
834
1027
|
description: 'Look up the safety score and risk category for MANY extensions at once, addressed by their ' +
|
|
835
1028
|
'STORE ids (the id in the store URL) — the inventory case: you have a list of what a user has ' +
|
|
836
1029
|
`installed and need risk for all of it. Up to ${MAX_BATCH_EXTENSIONS} extensions per call, and ` +
|
|
837
|
-
'the WHOLE call costs 1 read credit (not 1 per extension
|
|
838
|
-
'
|
|
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 ' +
|
|
839
1032
|
'= safer, the same number the website shows), risk category, severity counts, last scan date ' +
|
|
840
1033
|
'and the catalog URL per extension. Each store id is resolved through its cross-store cluster ' +
|
|
841
1034
|
'to the same listing the extension page renders, so these answers agree with the website. ' +
|
|
842
1035
|
'`store` is REQUIRED per entry — Chrome and Edge ids share one format. An extension with no ' +
|
|
843
1036
|
'score has not been scanned yet (`scanned: false`); ids with no catalog listing come back ' +
|
|
844
|
-
'under `notInCatalog`. Neither means "safe".
|
|
845
|
-
'get_security
|
|
1037
|
+
'under `notInCatalog`. Neither means "safe". The full findings list of ONE extension comes ' +
|
|
1038
|
+
'from get_security.',
|
|
846
1039
|
parameters: z.object({
|
|
847
1040
|
extensions: z
|
|
848
1041
|
.array(z.object({
|
|
@@ -857,8 +1050,8 @@ export function registerTools(server, deps) {
|
|
|
857
1050
|
}))
|
|
858
1051
|
.min(1)
|
|
859
1052
|
.max(MAX_BATCH_EXTENSIONS)
|
|
860
|
-
.describe(`The extensions to look up (max ${MAX_BATCH_EXTENSIONS}).
|
|
861
|
-
`
|
|
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.`),
|
|
862
1055
|
}),
|
|
863
1056
|
execute: async (args, context) => {
|
|
864
1057
|
try {
|
|
@@ -882,10 +1075,22 @@ export function registerTools(server, deps) {
|
|
|
882
1075
|
query: z.string().optional().describe('Optional query to scope facets to a search.'),
|
|
883
1076
|
stores: z.array(z.enum(['CHROME', 'FIREFOX', 'EDGE'])).optional(),
|
|
884
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.`),
|
|
885
1089
|
}),
|
|
886
1090
|
execute: async (args, context) => {
|
|
887
1091
|
try {
|
|
888
1092
|
const client = bff(context);
|
|
1093
|
+
const shapeOpts = { facets: args.facets, topN: args.top_n };
|
|
889
1094
|
const scoped = Boolean(args.query?.trim() || args.stores?.length || args.categories?.length);
|
|
890
1095
|
// Search-scoped: progressive-narrowing facets over the match set.
|
|
891
1096
|
if (scoped) {
|
|
@@ -895,11 +1100,7 @@ export function registerTools(server, deps) {
|
|
|
895
1100
|
.getSearchFacets({ query: args.query, stores: args.stores, categories: args.categories })
|
|
896
1101
|
.catch(() => null),
|
|
897
1102
|
]);
|
|
898
|
-
return renderCatalogPayload({
|
|
899
|
-
scope: 'search',
|
|
900
|
-
stats: stats ?? undefined,
|
|
901
|
-
facets: facets ?? undefined,
|
|
902
|
-
});
|
|
1103
|
+
return renderCatalogPayload(shapeMarketOverview({ scope: 'search', stats: stats ?? undefined, facets: facets ?? undefined }, shapeOpts));
|
|
903
1104
|
}
|
|
904
1105
|
// Catalog-wide: the unscoped search facets are all-zero by design, so build the
|
|
905
1106
|
// overview from the dedicated catalog-wide procedures (cached server-side).
|
|
@@ -920,7 +1121,7 @@ export function registerTools(server, deps) {
|
|
|
920
1121
|
count: r.count,
|
|
921
1122
|
label: STORE_LABELS[r.store] ?? r.store,
|
|
922
1123
|
}));
|
|
923
|
-
return renderCatalogPayload({
|
|
1124
|
+
return renderCatalogPayload(shapeMarketOverview({
|
|
924
1125
|
scope: 'catalog-wide',
|
|
925
1126
|
stats: stats ?? undefined,
|
|
926
1127
|
facets: {
|
|
@@ -928,8 +1129,8 @@ export function registerTools(server, deps) {
|
|
|
928
1129
|
categoryTree: categoryTree ?? undefined,
|
|
929
1130
|
extended: extended ?? undefined,
|
|
930
1131
|
},
|
|
931
|
-
note: 'Monetization and download-volume facets are
|
|
932
|
-
});
|
|
1132
|
+
note: 'Monetization and download-volume facets are computed only for a query-scoped overview.',
|
|
1133
|
+
}, shapeOpts));
|
|
933
1134
|
}
|
|
934
1135
|
catch (err) {
|
|
935
1136
|
return readError(err, missingKeyMessage);
|
|
@@ -938,15 +1139,15 @@ export function registerTools(server, deps) {
|
|
|
938
1139
|
});
|
|
939
1140
|
add({
|
|
940
1141
|
name: 'get_credit_balance',
|
|
941
|
-
description: '
|
|
942
|
-
'
|
|
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 ' +
|
|
943
1144
|
'search_extensions / get_extension / get_reviews / market_overview / get_risk_by_store_ids — the ' +
|
|
944
1145
|
'last one covers up to 40 extensions for that single credit; get_security costs 3) and ' +
|
|
945
1146
|
'`scan` (spent by scan_extension), plus `icon` and `inventory`. Each pool reports `remaining` — the ' +
|
|
946
1147
|
'number that actually gates calls (one-time free grant + purchased credits) — and `freeRemaining` ' +
|
|
947
|
-
'(how much of the
|
|
948
|
-
'spends
|
|
949
|
-
'
|
|
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).',
|
|
950
1151
|
parameters: z.object({}),
|
|
951
1152
|
execute: async (_args, context) => {
|
|
952
1153
|
try {
|
|
@@ -969,9 +1170,9 @@ export function registerTools(server, deps) {
|
|
|
969
1170
|
name: 'list_my_projects',
|
|
970
1171
|
description: "List the extension projects owned by this API key — the developer's own workspaces on " +
|
|
971
1172
|
'extenshi.io. Returns id, name, status, target browsers, the bound GitHub repo (if any) and ' +
|
|
972
|
-
'the claimed store listing (if any).
|
|
973
|
-
'"my project": the id
|
|
974
|
-
'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.',
|
|
975
1176
|
parameters: z.object({}),
|
|
976
1177
|
execute: async (_args, context) => {
|
|
977
1178
|
try {
|
|
@@ -988,22 +1189,22 @@ export function registerTools(server, deps) {
|
|
|
988
1189
|
'they picked (popup / side panel / page enhancer / in-page assistant), the required ' +
|
|
989
1190
|
'permissions those types force, the exact manifest.json the project produces for each target ' +
|
|
990
1191
|
'browser, the files that type needs, live hosted URLs (uninstall survey), and an index of ' +
|
|
991
|
-
'every saved tool state with its size.
|
|
992
|
-
'
|
|
993
|
-
'
|
|
994
|
-
'
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
'in it. `integration.unwired`
|
|
998
|
-
'
|
|
999
|
-
'row can be 256 KiB);
|
|
1000
|
-
'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.',
|
|
1001
1202
|
parameters: z.object({
|
|
1002
1203
|
projectId: z.string().describe('Project id from list_my_projects.'),
|
|
1003
1204
|
includeToolStates: z
|
|
1004
1205
|
.array(z.string())
|
|
1005
1206
|
.optional()
|
|
1006
|
-
.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.'),
|
|
1007
1208
|
}),
|
|
1008
1209
|
execute: async (args, context) => {
|
|
1009
1210
|
try {
|
|
@@ -1023,18 +1224,18 @@ export function registerTools(server, deps) {
|
|
|
1023
1224
|
description: "The starter extension one of the developer's own projects produces, as FILES ready to " +
|
|
1024
1225
|
'write: manifest.json for the target browser, the background worker, the panel or ' +
|
|
1025
1226
|
"content-script files that project's types need, placeholder icons, and " +
|
|
1026
|
-
'src/extenshi.config.js.
|
|
1027
|
-
"
|
|
1028
|
-
'
|
|
1029
|
-
'
|
|
1030
|
-
"
|
|
1031
|
-
'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.',
|
|
1032
1233
|
parameters: z.object({
|
|
1033
1234
|
projectId: z.string().describe('Project id from list_my_projects.'),
|
|
1034
1235
|
browser: z
|
|
1035
1236
|
.enum(['chrome', 'firefox', 'edge'])
|
|
1036
1237
|
.optional()
|
|
1037
|
-
.describe("Target browser
|
|
1238
|
+
.describe("Target browser; defaults to the project's first declared target."),
|
|
1038
1239
|
}),
|
|
1039
1240
|
execute: async (args, context) => {
|
|
1040
1241
|
try {
|
|
@@ -1069,8 +1270,8 @@ export function registerTools(server, deps) {
|
|
|
1069
1270
|
});
|
|
1070
1271
|
add({
|
|
1071
1272
|
name: 'get_privacy_policy_version',
|
|
1072
|
-
description: 'Read the markdown (and HTML) of one hosted privacy-policy version
|
|
1073
|
-
'
|
|
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.',
|
|
1074
1275
|
parameters: z.object({
|
|
1075
1276
|
projectId: z.string().describe('Project id from list_my_projects.'),
|
|
1076
1277
|
versionNumber: z
|
|
@@ -1090,17 +1291,17 @@ export function registerTools(server, deps) {
|
|
|
1090
1291
|
});
|
|
1091
1292
|
add({
|
|
1092
1293
|
name: 'publish_privacy_policy',
|
|
1093
|
-
description: 'Publish a hosted privacy policy for a Pro project.
|
|
1094
|
-
'the saved form and manifest (
|
|
1095
|
-
'
|
|
1096
|
-
'
|
|
1097
|
-
'
|
|
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.',
|
|
1098
1299
|
parameters: z.object({
|
|
1099
1300
|
projectId: z.string().describe('Project id from list_my_projects.'),
|
|
1100
1301
|
bodyMarkdown: z
|
|
1101
1302
|
.string()
|
|
1102
1303
|
.optional()
|
|
1103
|
-
.describe('Full policy markdown.
|
|
1304
|
+
.describe('Full policy markdown. When omitted, the policy is generated from the saved form and manifest.'),
|
|
1104
1305
|
kind: z
|
|
1105
1306
|
.enum(['generated', 'edited', 'ai_updated', 'reverted'])
|
|
1106
1307
|
.optional()
|
|
@@ -1118,8 +1319,8 @@ export function registerTools(server, deps) {
|
|
|
1118
1319
|
add({
|
|
1119
1320
|
name: 'update_privacy_policy_with_ai',
|
|
1120
1321
|
description: "Propose an updated privacy policy that keeps the author's custom wording and adds sections " +
|
|
1121
|
-
'required by new permissions or data practices.
|
|
1122
|
-
'
|
|
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 ' +
|
|
1123
1324
|
'policy that is already published. Pro only.',
|
|
1124
1325
|
parameters: z.object({
|
|
1125
1326
|
projectId: z.string().describe('Project id from list_my_projects.'),
|
|
@@ -1138,40 +1339,55 @@ export function registerTools(server, deps) {
|
|
|
1138
1339
|
if (caps.has('docs')) {
|
|
1139
1340
|
add({
|
|
1140
1341
|
name: 'get_development_guide',
|
|
1141
|
-
description: '
|
|
1342
|
+
description: 'For extension-development tasks: returns the complete tool inventory for THIS connection, ' +
|
|
1142
1343
|
'the Extenshi service directory with access requirements and docs links, GitHub/code placement ' +
|
|
1143
1344
|
'guidance, and the ordered workflow from idea and repository through implementation, assets, ' +
|
|
1144
1345
|
'privacy, tests/CI, store submission and maintenance. Distinguishes MCP tools from cabinet, ' +
|
|
1145
1346
|
'CLI and external steps. Free; no key, account lookup or network request.',
|
|
1146
|
-
parameters: z.object({
|
|
1147
|
-
|
|
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),
|
|
1148
1355
|
});
|
|
1149
1356
|
add({
|
|
1150
1357
|
name: 'search_docs',
|
|
1151
1358
|
description: 'Search the official Extenshi documentation (docs.extenshi.io) — product guides plus the ' +
|
|
1152
|
-
'full @extenshi/cli command reference (scan, review-risk, publish, login).
|
|
1153
|
-
'"how do I…" questions and to
|
|
1154
|
-
'
|
|
1155
|
-
'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.',
|
|
1156
1362
|
parameters: z.object({
|
|
1157
1363
|
query: z
|
|
1158
1364
|
.string()
|
|
1159
1365
|
.optional()
|
|
1160
|
-
.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.'),
|
|
1161
1367
|
limit: z
|
|
1162
1368
|
.number()
|
|
1163
1369
|
.int()
|
|
1164
1370
|
.min(1)
|
|
1165
1371
|
.max(10)
|
|
1166
1372
|
.default(4)
|
|
1167
|
-
.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.`),
|
|
1168
1381
|
}),
|
|
1169
1382
|
execute: async (args) => {
|
|
1170
1383
|
try {
|
|
1171
1384
|
const query = args.query?.trim();
|
|
1172
1385
|
if (!query)
|
|
1173
1386
|
return await getDocsIndex(deps.cfg.docsUrl);
|
|
1174
|
-
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
|
+
});
|
|
1175
1391
|
}
|
|
1176
1392
|
catch (err) {
|
|
1177
1393
|
// Both branches keep the origin as `cause` — a docs outage is a
|
|
@@ -1182,6 +1398,16 @@ export function registerTools(server, deps) {
|
|
|
1182
1398
|
}
|
|
1183
1399
|
},
|
|
1184
1400
|
});
|
|
1401
|
+
add({
|
|
1402
|
+
name: 'localize_workflow',
|
|
1403
|
+
description: 'Get the FREE local browser-extension localization workflow: prepare missing or stale ' +
|
|
1404
|
+
'_locales messages, translate with your own coding agent, apply validated translations ' +
|
|
1405
|
+
'and check placeholders, protected terms and listing risks. Returns commands and the ' +
|
|
1406
|
+
'JSON handoff contract. Static guidance only: no API key, network or Extenshi credits; ' +
|
|
1407
|
+
'translation uses your agent provider. Includes release availability and human review gates.',
|
|
1408
|
+
parameters: z.object({}),
|
|
1409
|
+
execute: async () => renderLocalizeWorkflow(),
|
|
1410
|
+
});
|
|
1185
1411
|
add({
|
|
1186
1412
|
name: 'generate_icon_workflow',
|
|
1187
1413
|
description: 'Get the recommended FREE local workflow for creating a browser-extension icon: the ' +
|
|
@@ -1202,11 +1428,11 @@ export function registerTools(server, deps) {
|
|
|
1202
1428
|
add({
|
|
1203
1429
|
name: 'generate_welcome_page_workflow',
|
|
1204
1430
|
description: 'Get the design brief for a browser-extension welcome page — the page a user lands on ' +
|
|
1205
|
-
'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, ' +
|
|
1206
1432
|
'keep the new-tab change, use it on a site), exactly which illustrations to produce for ' +
|
|
1207
1433
|
'that goal, how to capture and crop them, how to place numbered/arrow markers showing ' +
|
|
1208
|
-
'where to click, the limits enforced on save, and the block JSON
|
|
1209
|
-
'
|
|
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 ' +
|
|
1210
1436
|
'API key, no network, no credits.',
|
|
1211
1437
|
parameters: z.object({
|
|
1212
1438
|
extension_name: z.string().max(120).optional().describe('Extension display name.'),
|
|
@@ -1258,16 +1484,16 @@ export function registerTools(server, deps) {
|
|
|
1258
1484
|
description: 'The kinds of browser extension you can build, with the permissions each one REQUIRES: ' +
|
|
1259
1485
|
'Popup (toolbar window), Side panel (docked beside the page), Page enhancer (runs on sites ' +
|
|
1260
1486
|
'you list), In-page assistant (your own UI over any site). Types combine, and the required ' +
|
|
1261
|
-
'permissions are the union.
|
|
1262
|
-
'
|
|
1263
|
-
'
|
|
1264
|
-
'
|
|
1265
|
-
"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.",
|
|
1266
1492
|
parameters: z.object({
|
|
1267
1493
|
types: z
|
|
1268
1494
|
.array(z.string())
|
|
1269
1495
|
.optional()
|
|
1270
|
-
.describe('
|
|
1496
|
+
.describe('Type ids to include (popup, sidepanel, content, overlay); all four when omitted.'),
|
|
1271
1497
|
}),
|
|
1272
1498
|
execute: async (args) => renderExtensionTemplates(args.types),
|
|
1273
1499
|
});
|
|
@@ -1331,14 +1557,15 @@ export function registerTools(server, deps) {
|
|
|
1331
1557
|
if (caps.has('publish')) {
|
|
1332
1558
|
add({
|
|
1333
1559
|
name: 'publish_extension',
|
|
1334
|
-
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. ' +
|
|
1335
1562
|
'FREE and fully local: the upload goes from this machine straight to the store APIs using store ' +
|
|
1336
1563
|
'credentials from the MCP server environment (CHROME_APP_ID/CHROME_CLIENT_ID/CHROME_CLIENT_SECRET/' +
|
|
1337
1564
|
'CHROME_REFRESH_TOKEN, FIREFOX_ADDON_GUID/FIREFOX_JWT_ISSUER/FIREFOX_JWT_SECRET, ' +
|
|
1338
1565
|
'EDGE_PRODUCT_ID/EDGE_CLIENT_ID/EDGE_CLIENT_SECRET/EDGE_TENANT_ID). The upload itself is local, but ' +
|
|
1339
|
-
'publishing is in an active testing phase: a quick Extenshi access check runs first (
|
|
1340
|
-
'
|
|
1341
|
-
'
|
|
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).',
|
|
1342
1569
|
parameters: z.object({
|
|
1343
1570
|
artifact_path: z
|
|
1344
1571
|
.string()
|