@extenshi/mcp 0.5.2 → 0.7.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 +60 -2
- package/dist/bff.d.ts +54 -1
- package/dist/bff.d.ts.map +1 -1
- package/dist/bff.js +25 -0
- package/dist/bff.js.map +1 -1
- package/dist/development-guide.d.ts +249 -0
- package/dist/development-guide.d.ts.map +1 -0
- package/dist/development-guide.js +273 -0
- package/dist/development-guide.js.map +1 -0
- package/dist/index.js +4 -3
- package/dist/index.js.map +1 -1
- package/dist/localize-workflow.d.ts +3 -0
- package/dist/localize-workflow.d.ts.map +1 -0
- package/dist/localize-workflow.js +87 -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/tools.d.ts +9 -5
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +419 -36
- package/dist/tools.js.map +1 -1
- package/package.json +8 -7
package/dist/tools.js
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* passes deps that read `context.session` (the validated OAuth identity).
|
|
14
14
|
*
|
|
15
15
|
* Capability gating: the remote connector deliberately exposes a SUBSET — the
|
|
16
|
-
*
|
|
16
|
+
* catalog/project/policy/guidance tools. `scan_extension` and `publish_extension` need
|
|
17
17
|
* the caller's LOCAL filesystem and LOCAL store credentials, which a hosted
|
|
18
18
|
* server has no access to (and must never store) — so they are registered only
|
|
19
19
|
* when the corresponding capability is present (stdio enables all four).
|
|
@@ -22,18 +22,24 @@
|
|
|
22
22
|
* capability → tools
|
|
23
23
|
* ─────────────────────────────────────────────────────────────
|
|
24
24
|
* 'read' → search_extensions, get_extension, get_reviews,
|
|
25
|
-
* get_security, get_risk_by_store_ids, market_overview
|
|
26
|
-
*
|
|
27
|
-
*
|
|
25
|
+
* get_security, get_risk_by_store_ids, market_overview,
|
|
26
|
+
* list_my_projects, get_project_state, get_project_scaffold,
|
|
27
|
+
* list/get/publish/update privacy policy (hosted; Pro)
|
|
28
|
+
* 'docs' → get_development_guide, search_docs, list_extension_templates, generate_icon_workflow,
|
|
29
|
+
* generate_welcome_page_workflow, localize_workflow (free; no key)
|
|
28
30
|
* 'scan' → scan_extension (local artifact; stdio only)
|
|
29
31
|
* 'publish' → publish_extension (local creds; stdio only)
|
|
30
32
|
*
|
|
31
33
|
* stdout is the MCP protocol channel for stdio — nothing here may write to it.
|
|
32
34
|
*/
|
|
35
|
+
import { agentEvidenceSchema, importManifestSchema, workspacePatchSchema } from '@extenshi/contracts';
|
|
33
36
|
import { UserError } from 'fastmcp';
|
|
34
37
|
import { z } from 'zod';
|
|
38
|
+
import { buildDevelopmentGuide } from './development-guide.js';
|
|
35
39
|
import { DocsError, getDocsIndex, searchDocs } from './docs.js';
|
|
36
40
|
import { renderIconWorkflow } from './icon-workflow.js';
|
|
41
|
+
import { renderLocalizeWorkflow } from './localize-workflow.js';
|
|
42
|
+
import { PAY_OPERATIONS, paySchemas } from './pay.js';
|
|
37
43
|
import { PublishSetupError, publishArtifact, readStoreCredentials, validateStoreCredentials, } from './publish.js';
|
|
38
44
|
import { checkPublishAccess } from './publish-access.js';
|
|
39
45
|
import { ScanError, scanArtifact } from './scan.js';
|
|
@@ -70,33 +76,33 @@ export const MISSING_KEY_MESSAGE = "This tool needs an Extenshi API key, and you
|
|
|
70
76
|
*/
|
|
71
77
|
export const MAX_BATCH_EXTENSIONS = 40;
|
|
72
78
|
export const SERVER_NAME = 'extenshi';
|
|
73
|
-
export const SERVER_INSTRUCTIONS = 'Extenshi
|
|
74
|
-
'
|
|
75
|
-
'
|
|
76
|
-
'
|
|
77
|
-
'
|
|
78
|
-
'
|
|
79
|
-
'
|
|
80
|
-
'
|
|
81
|
-
'
|
|
82
|
-
'
|
|
83
|
-
'
|
|
84
|
-
'
|
|
85
|
-
|
|
86
|
-
'
|
|
87
|
-
'
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
79
|
+
export const SERVER_INSTRUCTIONS = 'Extenshi helps develop and operate browser extensions. Start extension-development tasks with ' +
|
|
80
|
+
'get_development_guide (free): it lists every tool on this connection, the service directory, ' +
|
|
81
|
+
'account/local prerequisites, documentation links, GitHub repository guidance and the ordered ' +
|
|
82
|
+
'plan from scope and research through code, assets, privacy, CI, store release and maintenance. ' +
|
|
83
|
+
'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 ' +
|
|
84
|
+
'product details and exact CLI flags. With identity, call list_my_projects then get_project_state ' +
|
|
85
|
+
'to reuse the existing project and repository. Use list_extension_templates before the manifest, ' +
|
|
86
|
+
'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 ' +
|
|
87
|
+
'verbatim to integration.path, check integration.unwired and re-read state after cabinet edits. ' +
|
|
88
|
+
'Check get_credit_balance before metered work. get_risk_by_store_ids covers up to 40 store IDs ' +
|
|
89
|
+
'for one read; get_security costs three reads for detailed findings. Hosted policy tools require ' +
|
|
90
|
+
'Pro; publish_privacy_policy changes the live policy, while update_privacy_policy_with_ai returns ' +
|
|
91
|
+
"a proposal to review. Respect the author's authorization for publication and repository writes. " +
|
|
92
|
+
'Tool registration is not proof of account entitlements or a completed release. Verify hosted ' +
|
|
93
|
+
'URLs, built artifacts, store status and optional purchases before reporting completion. ' +
|
|
94
|
+
`API keys: ${KEY_PAGE}. Credits: ${BILLING_PAGE}.`;
|
|
95
|
+
/** Match initialize instructions to the same capabilities used for registration. */
|
|
96
|
+
export function getServerInstructions(capabilities) {
|
|
97
|
+
const local = [
|
|
98
|
+
capabilities.has('scan') ? 'scan_extension' : null,
|
|
99
|
+
capabilities.has('publish') ? 'publish_extension' : null,
|
|
100
|
+
].filter(Boolean);
|
|
101
|
+
return (SERVER_INSTRUCTIONS +
|
|
102
|
+
(local.length
|
|
103
|
+
? ` This connection exposes ${local.join(' and ')} for local artifacts; check credentials and access before use.`
|
|
104
|
+
: " This connection has no local artifact scan or store-publishing tool. Use the local stdio server or CLI on the developer's machine for those stages; hosted privacy-policy publishing is a separate capability."));
|
|
105
|
+
}
|
|
100
106
|
// ── Internal helpers (ported from index.ts, now deps-parametrized) ───────────
|
|
101
107
|
/**
|
|
102
108
|
* A `UserError` that keeps the error it renders as `cause`.
|
|
@@ -344,7 +350,7 @@ async function resolveOptionalExtensionId(client, args) {
|
|
|
344
350
|
* Anthropic Connectors Directory — its submission portal auto-syncs tools and
|
|
345
351
|
* refuses to submit any tool missing a `title` or a read/write hint. Kept as a
|
|
346
352
|
* name→annotation map (not inline per tool) so the read/write split is auditable
|
|
347
|
-
* in one place.
|
|
353
|
+
* in one place. Hosted policy publication writes a public page; scan uploads an artifact
|
|
348
354
|
* (not read-only, not destructive); publish writes to public stores (destructive).
|
|
349
355
|
*/
|
|
350
356
|
const TOOL_ANNOTATIONS = {
|
|
@@ -390,6 +396,12 @@ const TOOL_ANNOTATIONS = {
|
|
|
390
396
|
idempotentHint: true,
|
|
391
397
|
openWorldHint: true,
|
|
392
398
|
},
|
|
399
|
+
get_development_guide: {
|
|
400
|
+
title: 'Extension development services and workflow',
|
|
401
|
+
readOnlyHint: true,
|
|
402
|
+
idempotentHint: true,
|
|
403
|
+
openWorldHint: false,
|
|
404
|
+
},
|
|
393
405
|
search_docs: {
|
|
394
406
|
title: 'Search Extenshi documentation',
|
|
395
407
|
readOnlyHint: true,
|
|
@@ -420,6 +432,36 @@ const TOOL_ANNOTATIONS = {
|
|
|
420
432
|
idempotentHint: true,
|
|
421
433
|
openWorldHint: true,
|
|
422
434
|
},
|
|
435
|
+
list_privacy_policy_versions: {
|
|
436
|
+
title: 'List hosted privacy policy versions',
|
|
437
|
+
readOnlyHint: true,
|
|
438
|
+
idempotentHint: true,
|
|
439
|
+
openWorldHint: true,
|
|
440
|
+
},
|
|
441
|
+
get_privacy_policy_version: {
|
|
442
|
+
title: 'Read a privacy policy version',
|
|
443
|
+
readOnlyHint: true,
|
|
444
|
+
idempotentHint: true,
|
|
445
|
+
openWorldHint: true,
|
|
446
|
+
},
|
|
447
|
+
publish_privacy_policy: {
|
|
448
|
+
title: 'Publish a hosted privacy policy',
|
|
449
|
+
readOnlyHint: false,
|
|
450
|
+
destructiveHint: false,
|
|
451
|
+
openWorldHint: true,
|
|
452
|
+
},
|
|
453
|
+
update_privacy_policy_with_ai: {
|
|
454
|
+
title: 'Update a privacy policy with AI',
|
|
455
|
+
readOnlyHint: false,
|
|
456
|
+
destructiveHint: false,
|
|
457
|
+
openWorldHint: true,
|
|
458
|
+
},
|
|
459
|
+
localize_workflow: {
|
|
460
|
+
title: 'Local extension localization workflow',
|
|
461
|
+
readOnlyHint: true,
|
|
462
|
+
idempotentHint: true,
|
|
463
|
+
openWorldHint: false,
|
|
464
|
+
},
|
|
423
465
|
generate_icon_workflow: {
|
|
424
466
|
title: 'Icon design workflow guide',
|
|
425
467
|
readOnlyHint: true,
|
|
@@ -444,12 +486,38 @@ const TOOL_ANNOTATIONS = {
|
|
|
444
486
|
destructiveHint: true,
|
|
445
487
|
openWorldHint: true,
|
|
446
488
|
},
|
|
489
|
+
create_ci_ingest_secret: {
|
|
490
|
+
title: 'Create or rotate the CI evidence ingest secret',
|
|
491
|
+
readOnlyHint: false,
|
|
492
|
+
destructiveHint: true,
|
|
493
|
+
openWorldHint: true,
|
|
494
|
+
},
|
|
495
|
+
upsert_hosted_page: {
|
|
496
|
+
title: 'Register a hosted page URL',
|
|
497
|
+
readOnlyHint: false,
|
|
498
|
+
destructiveHint: false,
|
|
499
|
+
openWorldHint: true,
|
|
500
|
+
},
|
|
501
|
+
verify_hosted_artifact: {
|
|
502
|
+
title: 'Verify a hosted page matches its record',
|
|
503
|
+
readOnlyHint: false,
|
|
504
|
+
idempotentHint: true,
|
|
505
|
+
destructiveHint: false,
|
|
506
|
+
openWorldHint: true,
|
|
507
|
+
},
|
|
508
|
+
list_hosted_pages: {
|
|
509
|
+
title: 'List hosted page registrations',
|
|
510
|
+
readOnlyHint: true,
|
|
511
|
+
idempotentHint: true,
|
|
512
|
+
openWorldHint: true,
|
|
513
|
+
},
|
|
447
514
|
};
|
|
448
515
|
/**
|
|
449
|
-
* Register the Extenshi tools on a FastMCP server.
|
|
516
|
+
* Register the Extenshi tools once on a FastMCP server.
|
|
450
517
|
* Only the tools whose capability is present in `deps.capabilities` are added.
|
|
451
518
|
*/
|
|
452
519
|
export function registerTools(server, deps) {
|
|
520
|
+
const registeredTools = [];
|
|
453
521
|
const caps = deps.capabilities;
|
|
454
522
|
const missingKeyMessage = deps.missingKeyMessage ?? MISSING_KEY_MESSAGE;
|
|
455
523
|
// Generic so each tool's Zod `parameters` infers its own `args` type (a
|
|
@@ -457,8 +525,13 @@ export function registerTools(server, deps) {
|
|
|
457
525
|
function add(tool) {
|
|
458
526
|
// Attach the directory-required annotations (title + read/write hint) from
|
|
459
527
|
// the central map; an explicit `tool.annotations` (none today) still wins.
|
|
460
|
-
const annotations = {
|
|
528
|
+
const annotations = {
|
|
529
|
+
...TOOL_ANNOTATIONS[tool.name],
|
|
530
|
+
...tool.annotations,
|
|
531
|
+
title: tool.annotations?.title ?? TOOL_ANNOTATIONS[tool.name]?.title ?? tool.name.replaceAll('_', ' '),
|
|
532
|
+
};
|
|
461
533
|
server.addTool(instrument({ ...tool, annotations }));
|
|
534
|
+
registeredTools.push({ name: tool.name, description: tool.description ?? '', annotations });
|
|
462
535
|
}
|
|
463
536
|
// Per-call helpers bound to the injected deps.
|
|
464
537
|
const bff = (ctx) => deps.getBff(ctx);
|
|
@@ -469,6 +542,212 @@ export function registerTools(server, deps) {
|
|
|
469
542
|
};
|
|
470
543
|
// ── Read tools (free; key/identity required) ───────────────────────────────
|
|
471
544
|
if (caps.has('read')) {
|
|
545
|
+
for (const [method, name, , , mutation, description] of PAY_OPERATIONS) {
|
|
546
|
+
add({
|
|
547
|
+
name,
|
|
548
|
+
description: `${description} Requires ${mutation ? 'pay.write' : 'pay.read'} permission. Never put developer API keys or Stripe secrets in extension code.`,
|
|
549
|
+
parameters: paySchemas[method],
|
|
550
|
+
annotations: {
|
|
551
|
+
readOnlyHint: !mutation,
|
|
552
|
+
destructiveHint: mutation,
|
|
553
|
+
idempotentHint: !['createPayApp', 'connectPaySeller', 'rotatePayKey'].includes(method),
|
|
554
|
+
openWorldHint: true,
|
|
555
|
+
},
|
|
556
|
+
execute: async (args, context) => {
|
|
557
|
+
try {
|
|
558
|
+
const invoke = bff(context)[method];
|
|
559
|
+
return JSON.stringify(await invoke(args));
|
|
560
|
+
}
|
|
561
|
+
catch (err) {
|
|
562
|
+
if (err instanceof UserError && /^MCP_SCOPE_REQUIRED:pay\.(read|write)\./.test(err.message))
|
|
563
|
+
throw err;
|
|
564
|
+
const status = err?.data?.httpStatus;
|
|
565
|
+
const message = status === 403
|
|
566
|
+
? 'Pay access denied. Grant pay.read/pay.write explicitly and verify application ownership.'
|
|
567
|
+
: status === 412
|
|
568
|
+
? 'Pay prerequisites are incomplete. Read get_pay_readiness; the author completes legal acceptance and Stripe KYC in the browser.'
|
|
569
|
+
: status === 401
|
|
570
|
+
? 'Pay authentication failed. Reconnect or configure a developer API key with explicit Pay permissions.'
|
|
571
|
+
: 'Pay request failed. Check authentication, application readiness and backend support before retrying a mutation.';
|
|
572
|
+
// Provider failures may contain customer data or credentials; never echo them.
|
|
573
|
+
throw userErrorFrom(message, err);
|
|
574
|
+
}
|
|
575
|
+
},
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
add({
|
|
579
|
+
name: 'import_manifest',
|
|
580
|
+
description: 'Import an existing manifest.json into the Dojo manifest editor and project labels. Pass 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; apply with dryRun=false and that hash. Never infers data collection, prices, or publication. Requires project.write.',
|
|
581
|
+
parameters: importManifestSchema,
|
|
582
|
+
annotations: { readOnlyHint: false, idempotentHint: true, destructiveHint: true, openWorldHint: true },
|
|
583
|
+
execute: async (args, context) => {
|
|
584
|
+
try {
|
|
585
|
+
return JSON.stringify(await bff(context).importManifest(args));
|
|
586
|
+
}
|
|
587
|
+
catch (err) {
|
|
588
|
+
return readError(err, missingKeyMessage);
|
|
589
|
+
}
|
|
590
|
+
},
|
|
591
|
+
});
|
|
592
|
+
const reads = [
|
|
593
|
+
[
|
|
594
|
+
'get_project_workspace',
|
|
595
|
+
'Read revisioned source/built manifests, repository metadata, scope and release snapshot. These are observations, separate from scaffold drafts.',
|
|
596
|
+
'getProjectWorkspace',
|
|
597
|
+
],
|
|
598
|
+
[
|
|
599
|
+
'get_release_readiness',
|
|
600
|
+
'Explain readiness for the recorded browser and artifact by locale. Local or agent reports never become CI attestations. Stale evidence does not pass. Requires a Pro project.',
|
|
601
|
+
'getReleaseReadiness',
|
|
602
|
+
],
|
|
603
|
+
];
|
|
604
|
+
for (const [name, description, method] of reads)
|
|
605
|
+
add({
|
|
606
|
+
name,
|
|
607
|
+
description,
|
|
608
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
|
|
609
|
+
parameters: z.object({
|
|
610
|
+
projectId: z.string().uuid(),
|
|
611
|
+
browser: z
|
|
612
|
+
.enum(['chrome', 'firefox', 'edge'])
|
|
613
|
+
.optional()
|
|
614
|
+
.describe('Release status: select a browser from release history.'),
|
|
615
|
+
artifactDigest: z
|
|
616
|
+
.string()
|
|
617
|
+
.regex(/^[a-f0-9]{64}$/)
|
|
618
|
+
.optional()
|
|
619
|
+
.describe('Release status: select the exact artifact from history.'),
|
|
620
|
+
}),
|
|
621
|
+
execute: async (args, context) => {
|
|
622
|
+
try {
|
|
623
|
+
return JSON.stringify(await bff(context)[method](args));
|
|
624
|
+
}
|
|
625
|
+
catch (err) {
|
|
626
|
+
return readError(err, missingKeyMessage);
|
|
627
|
+
}
|
|
628
|
+
},
|
|
629
|
+
});
|
|
630
|
+
add({
|
|
631
|
+
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; never returns credentials or secrets. On auth failures run `extenshi login --recover` (CLI) or reissue the API key from dojo.extenshi.io/api-keys.',
|
|
633
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
|
|
634
|
+
parameters: z.object({}),
|
|
635
|
+
execute: async (_args, context) => {
|
|
636
|
+
try {
|
|
637
|
+
const client = bff(context);
|
|
638
|
+
const [connection, balance] = await Promise.all([
|
|
639
|
+
client.connectionDiagnostics(),
|
|
640
|
+
client.getApiCallerBalance(),
|
|
641
|
+
]);
|
|
642
|
+
return JSON.stringify({ connection, balance, registeredTools: registeredTools.map((t) => t.name) });
|
|
643
|
+
}
|
|
644
|
+
catch (err) {
|
|
645
|
+
return readError(err, missingKeyMessage);
|
|
646
|
+
}
|
|
647
|
+
},
|
|
648
|
+
});
|
|
649
|
+
for (const [name, method, dryRun] of [
|
|
650
|
+
['diff_project_state', 'diffProjectWorkspace', true],
|
|
651
|
+
['apply_project_patch', 'patchProjectWorkspace', false],
|
|
652
|
+
])
|
|
653
|
+
add({
|
|
654
|
+
name,
|
|
655
|
+
description: dryRun
|
|
656
|
+
? 'Preview a three-way metadata diff against a server-held base revision. No writes.'
|
|
657
|
+
: 'Apply a typed project metadata patch with expectedRevision and idempotencyKey. Preserve source and built manifests separately, including unknown keys. Missing fields never delete; tombstones explicitly delete. No source upload or scaffold overwrite. Requires project.write OAuth scope.',
|
|
658
|
+
annotations: {
|
|
659
|
+
readOnlyHint: dryRun,
|
|
660
|
+
idempotentHint: true,
|
|
661
|
+
destructiveHint: !dryRun,
|
|
662
|
+
openWorldHint: true,
|
|
663
|
+
},
|
|
664
|
+
parameters: workspacePatchSchema,
|
|
665
|
+
execute: async (args, context) => {
|
|
666
|
+
try {
|
|
667
|
+
return JSON.stringify(await bff(context)[method](args));
|
|
668
|
+
}
|
|
669
|
+
catch (err) {
|
|
670
|
+
return readError(err, missingKeyMessage);
|
|
671
|
+
}
|
|
672
|
+
},
|
|
673
|
+
});
|
|
674
|
+
add({
|
|
675
|
+
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 displayed ONCE — put it into the repository GitHub Actions secret EXTENSHI_EVIDENCE_SECRET. Creating again invalidates the previous secret. Requires project.write.',
|
|
677
|
+
annotations: { readOnlyHint: false, idempotentHint: false, destructiveHint: true, openWorldHint: true },
|
|
678
|
+
parameters: z.object({ projectId: z.string().uuid() }),
|
|
679
|
+
execute: async (args, context) => {
|
|
680
|
+
try {
|
|
681
|
+
return JSON.stringify(await bff(context).createCiIngestSecret(args));
|
|
682
|
+
}
|
|
683
|
+
catch (err) {
|
|
684
|
+
return readError(err, missingKeyMessage);
|
|
685
|
+
}
|
|
686
|
+
},
|
|
687
|
+
});
|
|
688
|
+
add({
|
|
689
|
+
name: 'upsert_hosted_page',
|
|
690
|
+
description: "Register the project's homepage or support page URL (HTTPS only, must be publicly reachable; the server hashes the body). The registered homepage URL feeds HOMEPAGE_URL when the integration config file is regenerated. Requires hosted.write.",
|
|
691
|
+
annotations: { readOnlyHint: false, idempotentHint: true, destructiveHint: false, openWorldHint: true },
|
|
692
|
+
parameters: z.object({
|
|
693
|
+
projectId: z.string().uuid(),
|
|
694
|
+
kind: z.enum(['homepage', 'support']),
|
|
695
|
+
url: z.string().url().max(2000),
|
|
696
|
+
}),
|
|
697
|
+
execute: async (args, context) => {
|
|
698
|
+
try {
|
|
699
|
+
return JSON.stringify(await bff(context).upsertHostedPage(args));
|
|
700
|
+
}
|
|
701
|
+
catch (err) {
|
|
702
|
+
return readError(err, missingKeyMessage);
|
|
703
|
+
}
|
|
704
|
+
},
|
|
705
|
+
});
|
|
706
|
+
add({
|
|
707
|
+
name: 'verify_hosted_artifact',
|
|
708
|
+
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.",
|
|
709
|
+
annotations: { readOnlyHint: false, idempotentHint: true, destructiveHint: false, openWorldHint: true },
|
|
710
|
+
parameters: z.object({
|
|
711
|
+
projectId: z.string().uuid(),
|
|
712
|
+
kind: z.enum(['homepage', 'support', 'privacy']),
|
|
713
|
+
}),
|
|
714
|
+
execute: async (args, context) => {
|
|
715
|
+
try {
|
|
716
|
+
return JSON.stringify(await bff(context).verifyHostedArtifact(args));
|
|
717
|
+
}
|
|
718
|
+
catch (err) {
|
|
719
|
+
return readError(err, missingKeyMessage);
|
|
720
|
+
}
|
|
721
|
+
},
|
|
722
|
+
});
|
|
723
|
+
add({
|
|
724
|
+
name: 'list_hosted_pages',
|
|
725
|
+
description: 'List the project registered hosted pages (homepage/support) with their verification status, content hash and last check time.',
|
|
726
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
|
|
727
|
+
parameters: z.object({ projectId: z.string().uuid() }),
|
|
728
|
+
execute: async (args, context) => {
|
|
729
|
+
try {
|
|
730
|
+
return JSON.stringify(await bff(context).listHostedPages(args));
|
|
731
|
+
}
|
|
732
|
+
catch (err) {
|
|
733
|
+
return readError(err, missingKeyMessage);
|
|
734
|
+
}
|
|
735
|
+
},
|
|
736
|
+
});
|
|
737
|
+
add({
|
|
738
|
+
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, never CI — CI evidence is written by the verified ingest endpoint only. Attachment files upload via the CLI (extenshi evidence push --attach); this tool records metadata only. Requires evidence.write OAuth scope.',
|
|
740
|
+
annotations: { readOnlyHint: false, idempotentHint: true, destructiveHint: false, openWorldHint: true },
|
|
741
|
+
parameters: agentEvidenceSchema,
|
|
742
|
+
execute: async (args, context) => {
|
|
743
|
+
try {
|
|
744
|
+
return JSON.stringify(await bff(context).recordProjectEvidence(args));
|
|
745
|
+
}
|
|
746
|
+
catch (err) {
|
|
747
|
+
return readError(err, missingKeyMessage);
|
|
748
|
+
}
|
|
749
|
+
},
|
|
750
|
+
});
|
|
472
751
|
add({
|
|
473
752
|
name: 'search_extensions',
|
|
474
753
|
description: 'Search the cross-store extension catalog (Chrome, Firefox, Edge) with hybrid relevance. ' +
|
|
@@ -840,7 +1119,7 @@ export function registerTools(server, deps) {
|
|
|
840
1119
|
'browser, the files that type needs, live hosted URLs (uninstall survey), and an index of ' +
|
|
841
1120
|
'every saved tool state with its size. Use it before writing code so the manifest you ship ' +
|
|
842
1121
|
'matches what the developer configured on the site — and re-read it after they change ' +
|
|
843
|
-
'something rather than assuming. It also returns the integration contract:
|
|
1122
|
+
'something rather than assuming. The additive workspace envelope holds revisioned local observations separately from these scaffold drafts. It also returns the integration contract: review local changes before writing ' +
|
|
844
1123
|
'`integration.file` to `integration.path` VERBATIM to wire the extension to this project — ' +
|
|
845
1124
|
"those bytes carry a fingerprint extenshi.io uses to tell the developer's edits from its " +
|
|
846
1125
|
'own, so an equivalent file you assemble yourself makes the site stop managing every value ' +
|
|
@@ -875,7 +1154,7 @@ export function registerTools(server, deps) {
|
|
|
875
1154
|
"content-script files that project's types need, placeholder icons, and " +
|
|
876
1155
|
'src/extenshi.config.js. Write the files VERBATIM — the config carries a fingerprint ' +
|
|
877
1156
|
"that lets extenshi.io keep recognising the repository as this project's, and rebuilding " +
|
|
878
|
-
'it by hand makes the site stop managing those values. Prefer this over writing a manifest ' +
|
|
1157
|
+
'it by hand makes the site stop managing those values. Use this for a NEW extension; never overwrite an existing repository. Prefer this over writing a manifest ' +
|
|
879
1158
|
'yourself: it is the same set the site commits, so what you write and what the site ' +
|
|
880
1159
|
"expects cannot disagree. One browser per call (default: the project's first target). " +
|
|
881
1160
|
'FREE — never spends a credit.',
|
|
@@ -899,9 +1178,103 @@ export function registerTools(server, deps) {
|
|
|
899
1178
|
}
|
|
900
1179
|
},
|
|
901
1180
|
});
|
|
1181
|
+
add({
|
|
1182
|
+
name: 'list_privacy_policy_versions',
|
|
1183
|
+
description: "List hosted privacy-policy versions for one of the developer's Pro projects: version number, " +
|
|
1184
|
+
'kind (generated / edited / ai_updated / reverted), and which one is live. The public URL is ' +
|
|
1185
|
+
'in get_project_state hostedArtifacts.privacyPolicy. Pro only — a free project returns ' +
|
|
1186
|
+
'PROJECT_PREMIUM_REQUIRED. Does not spend a credit.',
|
|
1187
|
+
parameters: z.object({
|
|
1188
|
+
projectId: z.string().describe('Project id from list_my_projects.'),
|
|
1189
|
+
}),
|
|
1190
|
+
execute: async (args, context) => {
|
|
1191
|
+
try {
|
|
1192
|
+
return JSON.stringify(await bff(context).listPrivacyPolicyVersions(args), null, 2);
|
|
1193
|
+
}
|
|
1194
|
+
catch (err) {
|
|
1195
|
+
return readError(err, missingKeyMessage);
|
|
1196
|
+
}
|
|
1197
|
+
},
|
|
1198
|
+
});
|
|
1199
|
+
add({
|
|
1200
|
+
name: 'get_privacy_policy_version',
|
|
1201
|
+
description: 'Read the markdown (and HTML) of one hosted privacy-policy version. Use list_privacy_policy_versions ' +
|
|
1202
|
+
'first. Pro only. Does not spend a credit.',
|
|
1203
|
+
parameters: z.object({
|
|
1204
|
+
projectId: z.string().describe('Project id from list_my_projects.'),
|
|
1205
|
+
versionNumber: z
|
|
1206
|
+
.number()
|
|
1207
|
+
.int()
|
|
1208
|
+
.positive()
|
|
1209
|
+
.describe('Version number from list_privacy_policy_versions.'),
|
|
1210
|
+
}),
|
|
1211
|
+
execute: async (args, context) => {
|
|
1212
|
+
try {
|
|
1213
|
+
return JSON.stringify(await bff(context).getPrivacyPolicyVersion(args), null, 2);
|
|
1214
|
+
}
|
|
1215
|
+
catch (err) {
|
|
1216
|
+
return readError(err, missingKeyMessage);
|
|
1217
|
+
}
|
|
1218
|
+
},
|
|
1219
|
+
});
|
|
1220
|
+
add({
|
|
1221
|
+
name: 'publish_privacy_policy',
|
|
1222
|
+
description: 'Publish a hosted privacy policy for a Pro project. Omit bodyMarkdown to generate version 1 from ' +
|
|
1223
|
+
'the saved form and manifest (always the first version). Pass bodyMarkdown to publish an edited ' +
|
|
1224
|
+
'(or AI-updated) draft — that goes live at the same public URL. After a successful publish, ' +
|
|
1225
|
+
're-read get_project_state so PRIVACY_POLICY_URL in integration.file is the hosted URL. ' +
|
|
1226
|
+
'Show the author the URL before telling them it is live. Pro only.',
|
|
1227
|
+
parameters: z.object({
|
|
1228
|
+
projectId: z.string().describe('Project id from list_my_projects.'),
|
|
1229
|
+
bodyMarkdown: z
|
|
1230
|
+
.string()
|
|
1231
|
+
.optional()
|
|
1232
|
+
.describe('Full policy markdown. Omit to generate from current form + manifest.'),
|
|
1233
|
+
kind: z
|
|
1234
|
+
.enum(['generated', 'edited', 'ai_updated', 'reverted'])
|
|
1235
|
+
.optional()
|
|
1236
|
+
.describe('How this version was produced. Default: generated if no markdown, edited if markdown is set.'),
|
|
1237
|
+
}),
|
|
1238
|
+
execute: async (args, context) => {
|
|
1239
|
+
try {
|
|
1240
|
+
return JSON.stringify(await bff(context).publishPrivacyPolicy(args), null, 2);
|
|
1241
|
+
}
|
|
1242
|
+
catch (err) {
|
|
1243
|
+
return readError(err, missingKeyMessage);
|
|
1244
|
+
}
|
|
1245
|
+
},
|
|
1246
|
+
});
|
|
1247
|
+
add({
|
|
1248
|
+
name: 'update_privacy_policy_with_ai',
|
|
1249
|
+
description: "Propose an updated privacy policy that keeps the author's custom wording and adds sections " +
|
|
1250
|
+
'required by new permissions or data practices. Does NOT go live — show proposedMarkdown to the ' +
|
|
1251
|
+
'author, then call publish_privacy_policy with that markdown (kind ai_updated). Requires a ' +
|
|
1252
|
+
'policy that is already published. Pro only.',
|
|
1253
|
+
parameters: z.object({
|
|
1254
|
+
projectId: z.string().describe('Project id from list_my_projects.'),
|
|
1255
|
+
}),
|
|
1256
|
+
execute: async (args, context) => {
|
|
1257
|
+
try {
|
|
1258
|
+
return JSON.stringify(await bff(context).updatePrivacyPolicyWithAi(args), null, 2);
|
|
1259
|
+
}
|
|
1260
|
+
catch (err) {
|
|
1261
|
+
return readError(err, missingKeyMessage);
|
|
1262
|
+
}
|
|
1263
|
+
},
|
|
1264
|
+
});
|
|
902
1265
|
}
|
|
903
1266
|
// ── Documentation (free; no API key required) ──────────────────────────────
|
|
904
1267
|
if (caps.has('docs')) {
|
|
1268
|
+
add({
|
|
1269
|
+
name: 'get_development_guide',
|
|
1270
|
+
description: 'Start here when developing an extension: get the complete tool inventory for THIS connection, ' +
|
|
1271
|
+
'the Extenshi service directory with access requirements and docs links, GitHub/code placement ' +
|
|
1272
|
+
'guidance, and the ordered workflow from idea and repository through implementation, assets, ' +
|
|
1273
|
+
'privacy, tests/CI, store submission and maintenance. Distinguishes MCP tools from cabinet, ' +
|
|
1274
|
+
'CLI and external steps. Free; no key, account lookup or network request.',
|
|
1275
|
+
parameters: z.object({}),
|
|
1276
|
+
execute: async () => JSON.stringify(buildDevelopmentGuide(registeredTools), null, 2),
|
|
1277
|
+
});
|
|
905
1278
|
add({
|
|
906
1279
|
name: 'search_docs',
|
|
907
1280
|
description: 'Search the official Extenshi documentation (docs.extenshi.io) — product guides plus the ' +
|
|
@@ -938,6 +1311,16 @@ export function registerTools(server, deps) {
|
|
|
938
1311
|
}
|
|
939
1312
|
},
|
|
940
1313
|
});
|
|
1314
|
+
add({
|
|
1315
|
+
name: 'localize_workflow',
|
|
1316
|
+
description: 'Get the FREE local browser-extension localization workflow: prepare missing or stale ' +
|
|
1317
|
+
'_locales messages, translate with your own coding agent, apply validated translations ' +
|
|
1318
|
+
'and check placeholders, protected terms and listing risks. Returns commands and the ' +
|
|
1319
|
+
'JSON handoff contract. Static guidance only: no API key, network or Extenshi credits; ' +
|
|
1320
|
+
'translation uses your agent provider. Includes release availability and human review gates.',
|
|
1321
|
+
parameters: z.object({}),
|
|
1322
|
+
execute: async () => renderLocalizeWorkflow(),
|
|
1323
|
+
});
|
|
941
1324
|
add({
|
|
942
1325
|
name: 'generate_icon_workflow',
|
|
943
1326
|
description: 'Get the recommended FREE local workflow for creating a browser-extension icon: the ' +
|