@extenshi/mcp 0.5.1 → 0.6.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 +29 -2
- package/dist/bff.d.ts +28 -0
- package/dist/bff.d.ts.map +1 -1
- package/dist/bff.js +11 -0
- package/dist/bff.js.map +1 -1
- package/dist/development-guide.d.ts +237 -0
- package/dist/development-guide.d.ts.map +1 -0
- package/dist/development-guide.js +245 -0
- package/dist/development-guide.js.map +1 -0
- package/dist/index.js +4 -3
- package/dist/index.js.map +1 -1
- package/dist/project-workspace.d.ts +1976 -0
- package/dist/project-workspace.d.ts.map +1 -0
- package/dist/project-workspace.js +350 -0
- package/dist/project-workspace.js.map +1 -0
- package/dist/telemetry.d.ts +9 -1
- package/dist/telemetry.d.ts.map +1 -1
- package/dist/telemetry.js +20 -2
- package/dist/telemetry.js.map +1 -1
- package/dist/tools.d.ts +8 -4
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +314 -42
- package/dist/tools.js.map +1 -1
- package/package.json +5 -1
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,8 +22,10 @@
|
|
|
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
|
-
*
|
|
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,
|
|
27
29
|
* generate_welcome_page_workflow (free; no key)
|
|
28
30
|
* 'scan' → scan_extension (local artifact; stdio only)
|
|
29
31
|
* 'publish' → publish_extension (local creds; stdio only)
|
|
@@ -32,8 +34,10 @@
|
|
|
32
34
|
*/
|
|
33
35
|
import { UserError } from 'fastmcp';
|
|
34
36
|
import { z } from 'zod';
|
|
37
|
+
import { buildDevelopmentGuide } from './development-guide.js';
|
|
35
38
|
import { DocsError, getDocsIndex, searchDocs } from './docs.js';
|
|
36
39
|
import { renderIconWorkflow } from './icon-workflow.js';
|
|
40
|
+
import { evidenceSchema, importManifestSchema, workspacePatchSchema } from './project-workspace.js';
|
|
37
41
|
import { PublishSetupError, publishArtifact, readStoreCredentials, validateStoreCredentials, } from './publish.js';
|
|
38
42
|
import { checkPublishAccess } from './publish-access.js';
|
|
39
43
|
import { ScanError, scanArtifact } from './scan.js';
|
|
@@ -70,33 +74,33 @@ export const MISSING_KEY_MESSAGE = "This tool needs an Extenshi API key, and you
|
|
|
70
74
|
*/
|
|
71
75
|
export const MAX_BATCH_EXTENSIONS = 40;
|
|
72
76
|
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
|
-
|
|
77
|
+
export const SERVER_INSTRUCTIONS = 'Extenshi helps develop and operate browser extensions. Start extension-development tasks with ' +
|
|
78
|
+
'get_development_guide (free): it lists every tool on this connection, the service directory, ' +
|
|
79
|
+
'account/local prerequisites, documentation links, GitHub repository guidance and the ordered ' +
|
|
80
|
+
'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 ' +
|
|
82
|
+
'product details and exact CLI flags. With identity, call list_my_projects then get_project_state ' +
|
|
83
|
+
'to reuse the existing project and repository. Use list_extension_templates before the manifest, ' +
|
|
84
|
+
'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 ' +
|
|
85
|
+
'verbatim to integration.path, check integration.unwired and re-read state after cabinet edits. ' +
|
|
86
|
+
'Check get_credit_balance before metered work. get_risk_by_store_ids covers up to 40 store IDs ' +
|
|
87
|
+
'for one read; get_security costs three reads for detailed findings. Hosted policy tools require ' +
|
|
88
|
+
'Pro; publish_privacy_policy changes the live policy, while update_privacy_policy_with_ai returns ' +
|
|
89
|
+
"a proposal to review. Respect the author's authorization for publication and repository writes. " +
|
|
90
|
+
'Tool registration is not proof of account entitlements or a completed release. Verify hosted ' +
|
|
91
|
+
'URLs, built artifacts, store status and optional purchases before reporting completion. ' +
|
|
92
|
+
`API keys: ${KEY_PAGE}. Credits: ${BILLING_PAGE}.`;
|
|
93
|
+
/** Match initialize instructions to the same capabilities used for registration. */
|
|
94
|
+
export function getServerInstructions(capabilities) {
|
|
95
|
+
const local = [
|
|
96
|
+
capabilities.has('scan') ? 'scan_extension' : null,
|
|
97
|
+
capabilities.has('publish') ? 'publish_extension' : null,
|
|
98
|
+
].filter(Boolean);
|
|
99
|
+
return (SERVER_INSTRUCTIONS +
|
|
100
|
+
(local.length
|
|
101
|
+
? ` This connection exposes ${local.join(' and ')} for local artifacts; check credentials and access before use.`
|
|
102
|
+
: " 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."));
|
|
103
|
+
}
|
|
100
104
|
// ── Internal helpers (ported from index.ts, now deps-parametrized) ───────────
|
|
101
105
|
/**
|
|
102
106
|
* A `UserError` that keeps the error it renders as `cause`.
|
|
@@ -200,10 +204,36 @@ export function isExpectedError(err, kind = classifyError(err)) {
|
|
|
200
204
|
return err instanceof UserError && err.cause === undefined;
|
|
201
205
|
}
|
|
202
206
|
/**
|
|
203
|
-
*
|
|
204
|
-
*
|
|
205
|
-
*
|
|
206
|
-
*
|
|
207
|
+
* Attribution for a tool call, lifted from the session the surface supplies.
|
|
208
|
+
*
|
|
209
|
+
* Local stdio has no session at all, so both fields come back undefined and
|
|
210
|
+
* captureEvent falls back to the per-install anonymous id — unchanged behaviour.
|
|
211
|
+
* The hosted connector authenticates every request, so it supplies both: the
|
|
212
|
+
* account id becomes the PostHog distinct_id, and the connection id threads one
|
|
213
|
+
* client's calls together (see tools/extenshi-mcp-server/src/identity.ts, which
|
|
214
|
+
* mints it per `authenticate()` — i.e. per connection, NOT per cached token).
|
|
215
|
+
*
|
|
216
|
+
* Read defensively: `session` is FastMCP's opaque auth record, and a surface
|
|
217
|
+
* that carries neither field must degrade to the local behaviour rather than
|
|
218
|
+
* throw inside instrumentation.
|
|
219
|
+
*/
|
|
220
|
+
function attribution(session) {
|
|
221
|
+
if (!session || typeof session !== 'object')
|
|
222
|
+
return {};
|
|
223
|
+
const s = session;
|
|
224
|
+
return {
|
|
225
|
+
userId: typeof s.userId === 'string' && s.userId ? s.userId : undefined,
|
|
226
|
+
sessionId: typeof s.sessionId === 'string' && s.sessionId ? s.sessionId : undefined,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Wrap a tool definition so every call is reported: which tool ran, how long it
|
|
231
|
+
* took, and how it failed (coarse error_kind + sanitized exception). On the
|
|
232
|
+
* hosted connector the report also carries WHO — the authenticated account id —
|
|
233
|
+
* and which connection it belonged to; on local stdio it stays anonymous. Never
|
|
234
|
+
* the arguments, never the result. The generic preserves the Zod-inferred
|
|
235
|
+
* `args` type — `parameters` fixes Params, so the inner execute stays as
|
|
236
|
+
* strongly typed as before.
|
|
207
237
|
* Fail-soft: telemetry never alters the tool's result or its thrown error.
|
|
208
238
|
*/
|
|
209
239
|
function instrument(tool) {
|
|
@@ -213,10 +243,12 @@ function instrument(tool) {
|
|
|
213
243
|
...tool,
|
|
214
244
|
execute: async (args, context) => {
|
|
215
245
|
const startedAt = Date.now();
|
|
216
|
-
|
|
246
|
+
const { userId, sessionId } = attribution(context.session);
|
|
247
|
+
const props = { tool: name, ...(sessionId ? { mcp_session_id: sessionId } : {}) };
|
|
248
|
+
captureEvent('mcp_tool_called', props, userId);
|
|
217
249
|
try {
|
|
218
250
|
const result = await original(args, context);
|
|
219
|
-
captureEvent('mcp_tool_succeeded', {
|
|
251
|
+
captureEvent('mcp_tool_succeeded', { ...props, duration_ms: Date.now() - startedAt }, userId);
|
|
220
252
|
return result;
|
|
221
253
|
}
|
|
222
254
|
catch (err) {
|
|
@@ -226,7 +258,7 @@ function instrument(tool) {
|
|
|
226
258
|
const kind = classifyError(err);
|
|
227
259
|
// The failure count is always tracked, so we keep visibility into
|
|
228
260
|
// how often callers hit each condition (incl. the billing gate).
|
|
229
|
-
captureEvent('mcp_tool_failed', {
|
|
261
|
+
captureEvent('mcp_tool_failed', { ...props, error_kind: kind, duration_ms: Date.now() - startedAt }, userId);
|
|
230
262
|
// …but only genuine faults are shipped as exceptions. Expected
|
|
231
263
|
// user-facing conditions (a quota gate, a bad key, a message we
|
|
232
264
|
// authored) are not bugs and must not open error-tracking issues.
|
|
@@ -316,7 +348,7 @@ async function resolveOptionalExtensionId(client, args) {
|
|
|
316
348
|
* Anthropic Connectors Directory — its submission portal auto-syncs tools and
|
|
317
349
|
* refuses to submit any tool missing a `title` or a read/write hint. Kept as a
|
|
318
350
|
* name→annotation map (not inline per tool) so the read/write split is auditable
|
|
319
|
-
* in one place.
|
|
351
|
+
* in one place. Hosted policy publication writes a public page; scan uploads an artifact
|
|
320
352
|
* (not read-only, not destructive); publish writes to public stores (destructive).
|
|
321
353
|
*/
|
|
322
354
|
const TOOL_ANNOTATIONS = {
|
|
@@ -362,6 +394,12 @@ const TOOL_ANNOTATIONS = {
|
|
|
362
394
|
idempotentHint: true,
|
|
363
395
|
openWorldHint: true,
|
|
364
396
|
},
|
|
397
|
+
get_development_guide: {
|
|
398
|
+
title: 'Extension development services and workflow',
|
|
399
|
+
readOnlyHint: true,
|
|
400
|
+
idempotentHint: true,
|
|
401
|
+
openWorldHint: false,
|
|
402
|
+
},
|
|
365
403
|
search_docs: {
|
|
366
404
|
title: 'Search Extenshi documentation',
|
|
367
405
|
readOnlyHint: true,
|
|
@@ -392,6 +430,30 @@ const TOOL_ANNOTATIONS = {
|
|
|
392
430
|
idempotentHint: true,
|
|
393
431
|
openWorldHint: true,
|
|
394
432
|
},
|
|
433
|
+
list_privacy_policy_versions: {
|
|
434
|
+
title: 'List hosted privacy policy versions',
|
|
435
|
+
readOnlyHint: true,
|
|
436
|
+
idempotentHint: true,
|
|
437
|
+
openWorldHint: true,
|
|
438
|
+
},
|
|
439
|
+
get_privacy_policy_version: {
|
|
440
|
+
title: 'Read a privacy policy version',
|
|
441
|
+
readOnlyHint: true,
|
|
442
|
+
idempotentHint: true,
|
|
443
|
+
openWorldHint: true,
|
|
444
|
+
},
|
|
445
|
+
publish_privacy_policy: {
|
|
446
|
+
title: 'Publish a hosted privacy policy',
|
|
447
|
+
readOnlyHint: false,
|
|
448
|
+
destructiveHint: false,
|
|
449
|
+
openWorldHint: true,
|
|
450
|
+
},
|
|
451
|
+
update_privacy_policy_with_ai: {
|
|
452
|
+
title: 'Update a privacy policy with AI',
|
|
453
|
+
readOnlyHint: false,
|
|
454
|
+
destructiveHint: false,
|
|
455
|
+
openWorldHint: true,
|
|
456
|
+
},
|
|
395
457
|
generate_icon_workflow: {
|
|
396
458
|
title: 'Icon design workflow guide',
|
|
397
459
|
readOnlyHint: true,
|
|
@@ -418,10 +480,11 @@ const TOOL_ANNOTATIONS = {
|
|
|
418
480
|
},
|
|
419
481
|
};
|
|
420
482
|
/**
|
|
421
|
-
* Register the Extenshi tools on a FastMCP server.
|
|
483
|
+
* Register the Extenshi tools once on a FastMCP server.
|
|
422
484
|
* Only the tools whose capability is present in `deps.capabilities` are added.
|
|
423
485
|
*/
|
|
424
486
|
export function registerTools(server, deps) {
|
|
487
|
+
const registeredTools = [];
|
|
425
488
|
const caps = deps.capabilities;
|
|
426
489
|
const missingKeyMessage = deps.missingKeyMessage ?? MISSING_KEY_MESSAGE;
|
|
427
490
|
// Generic so each tool's Zod `parameters` infers its own `args` type (a
|
|
@@ -429,8 +492,13 @@ export function registerTools(server, deps) {
|
|
|
429
492
|
function add(tool) {
|
|
430
493
|
// Attach the directory-required annotations (title + read/write hint) from
|
|
431
494
|
// the central map; an explicit `tool.annotations` (none today) still wins.
|
|
432
|
-
const annotations = {
|
|
495
|
+
const annotations = {
|
|
496
|
+
...TOOL_ANNOTATIONS[tool.name],
|
|
497
|
+
...tool.annotations,
|
|
498
|
+
title: tool.annotations?.title ?? TOOL_ANNOTATIONS[tool.name]?.title ?? tool.name.replaceAll('_', ' '),
|
|
499
|
+
};
|
|
433
500
|
server.addTool(instrument({ ...tool, annotations }));
|
|
501
|
+
registeredTools.push({ name: tool.name, description: tool.description ?? '', annotations });
|
|
434
502
|
}
|
|
435
503
|
// Per-call helpers bound to the injected deps.
|
|
436
504
|
const bff = (ctx) => deps.getBff(ctx);
|
|
@@ -441,6 +509,116 @@ export function registerTools(server, deps) {
|
|
|
441
509
|
};
|
|
442
510
|
// ── Read tools (free; key/identity required) ───────────────────────────────
|
|
443
511
|
if (caps.has('read')) {
|
|
512
|
+
add({
|
|
513
|
+
name: 'import_manifest',
|
|
514
|
+
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.',
|
|
515
|
+
parameters: importManifestSchema,
|
|
516
|
+
annotations: { readOnlyHint: false, idempotentHint: true, destructiveHint: true, openWorldHint: true },
|
|
517
|
+
execute: async (args, context) => {
|
|
518
|
+
try {
|
|
519
|
+
return JSON.stringify(await bff(context).importManifest(args));
|
|
520
|
+
}
|
|
521
|
+
catch (err) {
|
|
522
|
+
return readError(err, missingKeyMessage);
|
|
523
|
+
}
|
|
524
|
+
},
|
|
525
|
+
});
|
|
526
|
+
const reads = [
|
|
527
|
+
[
|
|
528
|
+
'get_project_workspace',
|
|
529
|
+
'Read revisioned source/built manifests, repository metadata, scope and release snapshot. These are observations, separate from scaffold drafts.',
|
|
530
|
+
'getProjectWorkspace',
|
|
531
|
+
],
|
|
532
|
+
[
|
|
533
|
+
'get_release_readiness',
|
|
534
|
+
'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.',
|
|
535
|
+
'getReleaseReadiness',
|
|
536
|
+
],
|
|
537
|
+
];
|
|
538
|
+
for (const [name, description, method] of reads)
|
|
539
|
+
add({
|
|
540
|
+
name,
|
|
541
|
+
description,
|
|
542
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
|
|
543
|
+
parameters: z.object({
|
|
544
|
+
projectId: z.string().uuid(),
|
|
545
|
+
browser: z
|
|
546
|
+
.enum(['chrome', 'firefox', 'edge'])
|
|
547
|
+
.optional()
|
|
548
|
+
.describe('Release status: select a browser from release history.'),
|
|
549
|
+
artifactDigest: z
|
|
550
|
+
.string()
|
|
551
|
+
.regex(/^[a-f0-9]{64}$/)
|
|
552
|
+
.optional()
|
|
553
|
+
.describe('Release status: select the exact artifact from history.'),
|
|
554
|
+
}),
|
|
555
|
+
execute: async (args, context) => {
|
|
556
|
+
try {
|
|
557
|
+
return JSON.stringify(await bff(context)[method](args));
|
|
558
|
+
}
|
|
559
|
+
catch (err) {
|
|
560
|
+
return readError(err, missingKeyMessage);
|
|
561
|
+
}
|
|
562
|
+
},
|
|
563
|
+
});
|
|
564
|
+
add({
|
|
565
|
+
name: 'connection_diagnostics',
|
|
566
|
+
description: 'Verify authentication, granted scopes, supported workspace contracts and actual write permissions. Includes credit balances; never returns credentials. On OAuth invalid_grant reconnect the client and serialize refresh in its credential store.',
|
|
567
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
|
|
568
|
+
parameters: z.object({}),
|
|
569
|
+
execute: async (_args, context) => {
|
|
570
|
+
try {
|
|
571
|
+
const client = bff(context);
|
|
572
|
+
const [connection, balance] = await Promise.all([
|
|
573
|
+
client.connectionDiagnostics(),
|
|
574
|
+
client.getApiCallerBalance(),
|
|
575
|
+
]);
|
|
576
|
+
return JSON.stringify({ connection, balance, registeredTools: registeredTools.map((t) => t.name) });
|
|
577
|
+
}
|
|
578
|
+
catch (err) {
|
|
579
|
+
return readError(err, missingKeyMessage);
|
|
580
|
+
}
|
|
581
|
+
},
|
|
582
|
+
});
|
|
583
|
+
for (const [name, method, dryRun] of [
|
|
584
|
+
['diff_project_state', 'diffProjectWorkspace', true],
|
|
585
|
+
['apply_project_patch', 'patchProjectWorkspace', false],
|
|
586
|
+
])
|
|
587
|
+
add({
|
|
588
|
+
name,
|
|
589
|
+
description: dryRun
|
|
590
|
+
? 'Preview a three-way metadata diff against a server-held base revision. No writes.'
|
|
591
|
+
: '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.',
|
|
592
|
+
annotations: {
|
|
593
|
+
readOnlyHint: dryRun,
|
|
594
|
+
idempotentHint: true,
|
|
595
|
+
destructiveHint: !dryRun,
|
|
596
|
+
openWorldHint: true,
|
|
597
|
+
},
|
|
598
|
+
parameters: workspacePatchSchema,
|
|
599
|
+
execute: async (args, context) => {
|
|
600
|
+
try {
|
|
601
|
+
return JSON.stringify(await bff(context)[method](args));
|
|
602
|
+
}
|
|
603
|
+
catch (err) {
|
|
604
|
+
return readError(err, missingKeyMessage);
|
|
605
|
+
}
|
|
606
|
+
},
|
|
607
|
+
});
|
|
608
|
+
add({
|
|
609
|
+
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, never CI. No logs, screenshots or credentials uploaded. Requires evidence.write OAuth scope.',
|
|
611
|
+
annotations: { readOnlyHint: false, idempotentHint: true, destructiveHint: false, openWorldHint: true },
|
|
612
|
+
parameters: evidenceSchema,
|
|
613
|
+
execute: async (args, context) => {
|
|
614
|
+
try {
|
|
615
|
+
return JSON.stringify(await bff(context).recordProjectEvidence(args));
|
|
616
|
+
}
|
|
617
|
+
catch (err) {
|
|
618
|
+
return readError(err, missingKeyMessage);
|
|
619
|
+
}
|
|
620
|
+
},
|
|
621
|
+
});
|
|
444
622
|
add({
|
|
445
623
|
name: 'search_extensions',
|
|
446
624
|
description: 'Search the cross-store extension catalog (Chrome, Firefox, Edge) with hybrid relevance. ' +
|
|
@@ -812,7 +990,7 @@ export function registerTools(server, deps) {
|
|
|
812
990
|
'browser, the files that type needs, live hosted URLs (uninstall survey), and an index of ' +
|
|
813
991
|
'every saved tool state with its size. Use it before writing code so the manifest you ship ' +
|
|
814
992
|
'matches what the developer configured on the site — and re-read it after they change ' +
|
|
815
|
-
'something rather than assuming. It also returns the integration contract:
|
|
993
|
+
'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 ' +
|
|
816
994
|
'`integration.file` to `integration.path` VERBATIM to wire the extension to this project — ' +
|
|
817
995
|
"those bytes carry a fingerprint extenshi.io uses to tell the developer's edits from its " +
|
|
818
996
|
'own, so an equivalent file you assemble yourself makes the site stop managing every value ' +
|
|
@@ -847,7 +1025,7 @@ export function registerTools(server, deps) {
|
|
|
847
1025
|
"content-script files that project's types need, placeholder icons, and " +
|
|
848
1026
|
'src/extenshi.config.js. Write the files VERBATIM — the config carries a fingerprint ' +
|
|
849
1027
|
"that lets extenshi.io keep recognising the repository as this project's, and rebuilding " +
|
|
850
|
-
'it by hand makes the site stop managing those values. Prefer this over writing a manifest ' +
|
|
1028
|
+
'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 ' +
|
|
851
1029
|
'yourself: it is the same set the site commits, so what you write and what the site ' +
|
|
852
1030
|
"expects cannot disagree. One browser per call (default: the project's first target). " +
|
|
853
1031
|
'FREE — never spends a credit.',
|
|
@@ -871,9 +1049,103 @@ export function registerTools(server, deps) {
|
|
|
871
1049
|
}
|
|
872
1050
|
},
|
|
873
1051
|
});
|
|
1052
|
+
add({
|
|
1053
|
+
name: 'list_privacy_policy_versions',
|
|
1054
|
+
description: "List hosted privacy-policy versions for one of the developer's Pro projects: version number, " +
|
|
1055
|
+
'kind (generated / edited / ai_updated / reverted), and which one is live. The public URL is ' +
|
|
1056
|
+
'in get_project_state hostedArtifacts.privacyPolicy. Pro only — a free project returns ' +
|
|
1057
|
+
'PROJECT_PREMIUM_REQUIRED. Does not spend a credit.',
|
|
1058
|
+
parameters: z.object({
|
|
1059
|
+
projectId: z.string().describe('Project id from list_my_projects.'),
|
|
1060
|
+
}),
|
|
1061
|
+
execute: async (args, context) => {
|
|
1062
|
+
try {
|
|
1063
|
+
return JSON.stringify(await bff(context).listPrivacyPolicyVersions(args), null, 2);
|
|
1064
|
+
}
|
|
1065
|
+
catch (err) {
|
|
1066
|
+
return readError(err, missingKeyMessage);
|
|
1067
|
+
}
|
|
1068
|
+
},
|
|
1069
|
+
});
|
|
1070
|
+
add({
|
|
1071
|
+
name: 'get_privacy_policy_version',
|
|
1072
|
+
description: 'Read the markdown (and HTML) of one hosted privacy-policy version. Use list_privacy_policy_versions ' +
|
|
1073
|
+
'first. Pro only. Does not spend a credit.',
|
|
1074
|
+
parameters: z.object({
|
|
1075
|
+
projectId: z.string().describe('Project id from list_my_projects.'),
|
|
1076
|
+
versionNumber: z
|
|
1077
|
+
.number()
|
|
1078
|
+
.int()
|
|
1079
|
+
.positive()
|
|
1080
|
+
.describe('Version number from list_privacy_policy_versions.'),
|
|
1081
|
+
}),
|
|
1082
|
+
execute: async (args, context) => {
|
|
1083
|
+
try {
|
|
1084
|
+
return JSON.stringify(await bff(context).getPrivacyPolicyVersion(args), null, 2);
|
|
1085
|
+
}
|
|
1086
|
+
catch (err) {
|
|
1087
|
+
return readError(err, missingKeyMessage);
|
|
1088
|
+
}
|
|
1089
|
+
},
|
|
1090
|
+
});
|
|
1091
|
+
add({
|
|
1092
|
+
name: 'publish_privacy_policy',
|
|
1093
|
+
description: 'Publish a hosted privacy policy for a Pro project. Omit bodyMarkdown to generate version 1 from ' +
|
|
1094
|
+
'the saved form and manifest (always the first version). Pass bodyMarkdown to publish an edited ' +
|
|
1095
|
+
'(or AI-updated) draft — that goes live at the same public URL. After a successful publish, ' +
|
|
1096
|
+
're-read get_project_state so PRIVACY_POLICY_URL in integration.file is the hosted URL. ' +
|
|
1097
|
+
'Show the author the URL before telling them it is live. Pro only.',
|
|
1098
|
+
parameters: z.object({
|
|
1099
|
+
projectId: z.string().describe('Project id from list_my_projects.'),
|
|
1100
|
+
bodyMarkdown: z
|
|
1101
|
+
.string()
|
|
1102
|
+
.optional()
|
|
1103
|
+
.describe('Full policy markdown. Omit to generate from current form + manifest.'),
|
|
1104
|
+
kind: z
|
|
1105
|
+
.enum(['generated', 'edited', 'ai_updated', 'reverted'])
|
|
1106
|
+
.optional()
|
|
1107
|
+
.describe('How this version was produced. Default: generated if no markdown, edited if markdown is set.'),
|
|
1108
|
+
}),
|
|
1109
|
+
execute: async (args, context) => {
|
|
1110
|
+
try {
|
|
1111
|
+
return JSON.stringify(await bff(context).publishPrivacyPolicy(args), null, 2);
|
|
1112
|
+
}
|
|
1113
|
+
catch (err) {
|
|
1114
|
+
return readError(err, missingKeyMessage);
|
|
1115
|
+
}
|
|
1116
|
+
},
|
|
1117
|
+
});
|
|
1118
|
+
add({
|
|
1119
|
+
name: 'update_privacy_policy_with_ai',
|
|
1120
|
+
description: "Propose an updated privacy policy that keeps the author's custom wording and adds sections " +
|
|
1121
|
+
'required by new permissions or data practices. Does NOT go live — show proposedMarkdown to the ' +
|
|
1122
|
+
'author, then call publish_privacy_policy with that markdown (kind ai_updated). Requires a ' +
|
|
1123
|
+
'policy that is already published. Pro only.',
|
|
1124
|
+
parameters: z.object({
|
|
1125
|
+
projectId: z.string().describe('Project id from list_my_projects.'),
|
|
1126
|
+
}),
|
|
1127
|
+
execute: async (args, context) => {
|
|
1128
|
+
try {
|
|
1129
|
+
return JSON.stringify(await bff(context).updatePrivacyPolicyWithAi(args), null, 2);
|
|
1130
|
+
}
|
|
1131
|
+
catch (err) {
|
|
1132
|
+
return readError(err, missingKeyMessage);
|
|
1133
|
+
}
|
|
1134
|
+
},
|
|
1135
|
+
});
|
|
874
1136
|
}
|
|
875
1137
|
// ── Documentation (free; no API key required) ──────────────────────────────
|
|
876
1138
|
if (caps.has('docs')) {
|
|
1139
|
+
add({
|
|
1140
|
+
name: 'get_development_guide',
|
|
1141
|
+
description: 'Start here when developing an extension: get the complete tool inventory for THIS connection, ' +
|
|
1142
|
+
'the Extenshi service directory with access requirements and docs links, GitHub/code placement ' +
|
|
1143
|
+
'guidance, and the ordered workflow from idea and repository through implementation, assets, ' +
|
|
1144
|
+
'privacy, tests/CI, store submission and maintenance. Distinguishes MCP tools from cabinet, ' +
|
|
1145
|
+
'CLI and external steps. Free; no key, account lookup or network request.',
|
|
1146
|
+
parameters: z.object({}),
|
|
1147
|
+
execute: async () => JSON.stringify(buildDevelopmentGuide(registeredTools), null, 2),
|
|
1148
|
+
});
|
|
877
1149
|
add({
|
|
878
1150
|
name: 'search_docs',
|
|
879
1151
|
description: 'Search the official Extenshi documentation (docs.extenshi.io) — product guides plus the ' +
|