@extenshi/mcp 0.13.0 → 0.14.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/dist/tools.js CHANGED
@@ -52,10 +52,13 @@ import { amoCreateAllowed, PublishSetupError, publishArtifact, storeCredentialsF
52
52
  import { checkPublishAccess } from './publish-access.js';
53
53
  import { ScanError, scanArtifact } from './scan.js';
54
54
  import { describeStoreConstraints, validateSearchFilters } from './search-filters.js';
55
+ import { canCaptureSessionTelemetry } from './session-telemetry.js';
55
56
  import { DEFAULT_MARKET_TOP_N, MARKET_FACETS, MAX_MARKET_TOP_N, shapeExtension, shapeMarketOverview, shapeReviews, shapeSearch, shapeSecurity, shapeStoreRiskBatch, } from './shape.js';
56
57
  import { captureError, captureEvent, classifyError } from './telemetry.js';
57
58
  import { renderExtensionTemplates } from './templates.js';
59
+ import { registerUninstallFormTools } from './uninstall-form-tools.js';
58
60
  import { renderCatalogPayload } from './untrusted.js';
61
+ import { knownUpdate } from './update-check.js';
59
62
  import { renderWelcomeWorkflow } from './welcome-workflow.js';
60
63
  // ── Public links (shared by tool bodies + server instructions) ──────────────
61
64
  export const KEY_PAGE = 'https://dojo.extenshi.io/api-keys';
@@ -76,6 +79,12 @@ export const MISSING_KEY_MESSAGE = "This tool needs an Extenshi API key, and you
76
79
  'config (or run `extenshi login`).\n\n' +
77
80
  'Tip: the `search_docs` tool is free and needs no key — use it any time to ' +
78
81
  'look up product docs and CLI commands.';
82
+ /**
83
+ * The remote connector's counterpart of MISSING_KEY_MESSAGE. It signs in over
84
+ * OAuth, so a rejected request means the sign-in lapsed — there is no key to find.
85
+ */
86
+ export const RECONNECT_MESSAGE = 'The Extenshi sign-in for this connection is missing, has expired or was revoked. ' +
87
+ 'Reconnect the Extenshi connector in your MCP client and sign in again; no API key is needed.';
79
88
  /**
80
89
  * Max extensions `get_risk_by_store_ids` accepts per call.
81
90
  *
@@ -88,35 +97,35 @@ export const MISSING_KEY_MESSAGE = "This tool needs an Extenshi API key, and you
88
97
  */
89
98
  export const MAX_BATCH_EXTENSIONS = 40;
90
99
  export const SERVER_NAME = 'extenshi';
91
- export const SERVER_INSTRUCTIONS = 'Extenshi helps develop and operate browser extensions. Start extension-development tasks with ' +
92
- 'get_development_guide (free): it lists every tool on this connection, the service directory, ' +
93
- 'account/local prerequisites, documentation links, GitHub repository guidance and the ordered ' +
94
- 'plan from scope and research through code, assets, privacy, CI, store release and maintenance. ' +
95
- '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 ' +
96
- 'product details and exact CLI flags. With identity, call list_my_projects then get_project_state ' +
97
- 'to reuse the existing project and repository; for a new extension, create_project makes one (idempotent). ' +
98
- 'Read get_decisions early: the AMO add-on id, license, seller identity and pricing are owner decisions — ' +
99
- 'propose defaults with propose_decision; the owner decides them in Dojo, and release readiness blocks on them. ' +
100
- 'Use list_extension_templates before the manifest, ' +
101
- '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 its _locales messages (messagesByLocale) before configuring services. Review the integration diff and preserve local edits; write integration.file ' +
102
- 'verbatim to integration.path, check integration.unwired and re-read state after cabinet edits. ' +
103
- 'Check get_credit_balance before metered work. get_risk_by_store_ids covers up to 40 store IDs ' +
104
- 'for one read; get_security costs three reads for detailed findings. Hosted policy tools require ' +
105
- 'Pro; publish_privacy_policy changes the live policy, while update_privacy_policy_with_ai returns ' +
106
- "a proposal to review. Respect the author's authorization for publication and repository writes. " +
107
- 'Tool registration is not proof of account entitlements or a completed release. Verify hosted ' +
108
- 'URLs, built artifacts, store status and optional purchases before reporting completion. ' +
109
- `API keys: ${KEY_PAGE}. Credits: ${BILLING_PAGE}.`;
100
+ /**
101
+ * Shown to the model at `initialize`. It states what the tools return and what
102
+ * the account can do — not how to work — and is kept short: directory review
103
+ * reads this block in full, and a cut-off copy hides the end of it. The length
104
+ * is pinned by a test.
105
+ */
106
+ export const SERVER_INSTRUCTIONS = "Extenshi covers browser-extension development and operations: catalog and security data on other people's extensions, " +
107
+ "and the signed-in developer's own projects, hosted pages, Pay applications and privacy policy. " +
108
+ 'get_development_guide (free) returns the end-to-end workflow, the tools on this connection, the service directory and documentation links; ' +
109
+ 'search_docs (free) returns current product documentation and exact CLI flags. ' +
110
+ 'get_credit_balance reports the remaining read, scan, icon and inventory credits; get_risk_by_store_ids covers up to 40 store IDs for one read, and get_security costs three reads. ' +
111
+ "list_my_projects returns the developer's own projects (create_project adds one); get_project_state returns a project's configuration, including integration.file " +
112
+ '(the content for integration.path that links a repository to the project) and integration.unwired. ' +
113
+ 'get_decisions lists the owner decisions (AMO add-on id, license, seller identity, pricing); propose_decision proposes a value and the owner decides it in Dojo. ' +
114
+ 'Pay applications are independent of projects (list_pay_apps, create_pay_app, get_pay_readiness) and need the pay.read / pay.write scopes; ' +
115
+ 'agreement acceptance and KYC take place in the browser, and only the publishable SDK key and the public verification key belong in extension code. ' +
116
+ 'Hosted privacy-policy tools need a Pro project: publish_privacy_policy puts a policy on the live public page, update_privacy_policy_with_ai returns a proposal only. ' +
117
+ 'Registered tools are not proof of account entitlements or a completed release.';
110
118
  /** Match initialize instructions to the same capabilities used for registration. */
111
- export function getServerInstructions(capabilities) {
119
+ export function getServerInstructions(capabilities, auth = 'api-key') {
112
120
  const local = [
113
121
  capabilities.has('scan') ? 'scan_extension' : null,
114
122
  capabilities.has('publish') ? 'publish_extension' : null,
115
123
  ].filter(Boolean);
116
124
  return (SERVER_INSTRUCTIONS +
117
125
  (local.length
118
- ? ` This connection exposes ${local.join(' and ')} for local artifacts; check credentials and access before use.`
119
- : " 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."));
126
+ ? ` This connection exposes ${local.join(' and ')} for local artifacts; they need valid credentials and publishing access.`
127
+ : ' This connection has no local artifact scan or store-publishing tool; the local stdio server and the CLI provide those stages.') +
128
+ (auth === 'api-key' ? ` API keys for the local package: ${KEY_PAGE}.` : ''));
120
129
  }
121
130
  // ── Internal helpers (ported from index.ts, now deps-parametrized) ───────────
122
131
  /**
@@ -221,26 +230,15 @@ export function isExpectedError(err, kind = classifyError(err)) {
221
230
  return err instanceof UserError && err.cause === undefined;
222
231
  }
223
232
  /**
224
- * Attribution for a tool call, lifted from the session the surface supplies.
225
- *
226
- * Local stdio has no session at all, so both fields come back undefined and
227
- * captureEvent falls back to the per-install anonymous id — unchanged behaviour.
228
- * The hosted connector authenticates every request, so it supplies both: the
229
- * account id becomes the PostHog distinct_id, and the connection id threads one
230
- * client's calls together (see tools/extenshi-mcp-server/src/identity.ts, which
231
- * mints it per `authenticate()` — i.e. per connection, NOT per cached token).
232
- *
233
- * Read defensively: `session` is FastMCP's opaque auth record, and a surface
234
- * that carries neither field must degrade to the local behaviour rather than
235
- * throw inside instrumentation.
233
+ * Account identity comes from validated auth; connection identity comes from
234
+ * FastMCP's server-owned transport, not the auth object refreshed on each POST.
235
+ * Local stdio keeps captureEvent's anonymous per-install identity.
236
236
  */
237
- function attribution(session) {
238
- if (!session || typeof session !== 'object')
239
- return {};
240
- const s = session;
237
+ function attribution(session, sessionId) {
238
+ const s = session && typeof session === 'object' ? session : {};
241
239
  return {
242
240
  userId: typeof s.userId === 'string' && s.userId ? s.userId : undefined,
243
- sessionId: typeof s.sessionId === 'string' && s.sessionId ? s.sessionId : undefined,
241
+ sessionId: typeof sessionId === 'string' && sessionId ? sessionId : undefined,
244
242
  };
245
243
  }
246
244
  /**
@@ -279,15 +277,16 @@ function instrument(tool) {
279
277
  execute: async (args, context) => {
280
278
  // The hosted connector lets a user decline analytics on the consent
281
279
  // screen; honour it for every event, including error reports.
282
- if (context.session?.telemetry === false)
280
+ if (!canCaptureSessionTelemetry(context.session))
283
281
  return original(args, context);
284
282
  const startedAt = Date.now();
285
- const { userId, sessionId } = attribution(context.session);
283
+ const { userId, sessionId } = attribution(context.session, context.sessionId);
286
284
  const props = { tool: name, ...(sessionId ? { mcp_session_id: sessionId } : {}) };
287
285
  captureEvent('mcp_tool_called', props, userId);
288
286
  try {
289
287
  const result = await original(args, context);
290
- captureEvent('mcp_tool_succeeded', { ...props, duration_ms: Date.now() - startedAt }, userId);
288
+ if (canCaptureSessionTelemetry(context.session))
289
+ captureEvent('mcp_tool_succeeded', { ...props, duration_ms: Date.now() - startedAt }, userId);
291
290
  return result;
292
291
  }
293
292
  catch (err) {
@@ -297,12 +296,14 @@ function instrument(tool) {
297
296
  const kind = classifyError(err);
298
297
  // The failure count is always tracked, so we keep visibility into
299
298
  // how often callers hit each condition (incl. the billing gate).
300
- captureEvent('mcp_tool_failed', { ...props, error_kind: kind, duration_ms: Date.now() - startedAt }, userId);
301
- // …but only genuine faults are shipped as exceptions. Expected
302
- // user-facing conditions (a quota gate, a bad key, a message we
303
- // authored) are not bugs and must not open error-tracking issues.
304
- if (!isExpectedError(err, kind))
305
- captureError(err, { tool: name });
299
+ if (canCaptureSessionTelemetry(context.session)) {
300
+ captureEvent('mcp_tool_failed', { ...props, error_kind: kind, duration_ms: Date.now() - startedAt }, userId);
301
+ // …but only genuine faults are shipped as exceptions. Expected
302
+ // user-facing conditions (a quota gate, a bad key, a message we
303
+ // authored) are not bugs and must not open error-tracking issues.
304
+ if (!isExpectedError(err, kind))
305
+ captureError(err, { tool: name }, userId);
306
+ }
306
307
  throw err;
307
308
  }
308
309
  },
@@ -504,7 +505,8 @@ const TOOL_ANNOTATIONS = {
504
505
  publish_privacy_policy: {
505
506
  title: 'Publish a hosted privacy policy',
506
507
  readOnlyHint: false,
507
- destructiveHint: false,
508
+ // Overwrites the live public policy page, so a client treats it like any other overwrite.
509
+ destructiveHint: true,
508
510
  openWorldHint: true,
509
511
  },
510
512
  update_privacy_policy_with_ai: {
@@ -909,7 +911,8 @@ const LANDING_FORM_SHAPE = {
909
911
  export function registerTools(server, deps) {
910
912
  const registeredTools = [];
911
913
  const caps = deps.capabilities;
912
- const missingKeyMessage = deps.missingKeyMessage ?? MISSING_KEY_MESSAGE;
914
+ const oauth = deps.auth === 'oauth';
915
+ const missingKeyMessage = deps.missingKeyMessage ?? (oauth ? RECONNECT_MESSAGE : MISSING_KEY_MESSAGE);
913
916
  // Generic so each tool's Zod `parameters` infers its own `args` type (a
914
917
  // non-generic wrapper would collapse Params to `never`).
915
918
  function add(tool) {
@@ -967,7 +970,9 @@ export function registerTools(server, deps) {
967
970
  : status === 404
968
971
  ? 'Pay application or resource not found. Check the appId with list_pay_apps.'
969
972
  : status === 401
970
- ? 'Pay authentication failed. Reconnect or configure a developer API key with explicit Pay permissions.'
973
+ ? oauth
974
+ ? `Pay authentication failed. Reconnect the Extenshi connector and grant the ${mutation ? 'pay.write' : 'pay.read'} permission when asked.`
975
+ : 'Pay authentication failed. Reconnect or configure a developer API key with explicit Pay permissions.'
971
976
  : 'Pay request failed. Check authentication, application readiness and backend support before retrying a mutation.';
972
977
  // Provider failures may contain customer data or credentials; never echo them.
973
978
  throw userErrorFrom(message, err);
@@ -1031,11 +1036,16 @@ export function registerTools(server, deps) {
1031
1036
  description: 'Propose a value for one owner decision. The proposal is stored with its rationale, proposer and ' +
1032
1037
  'time; the owner accepts or edits it on the Dojo project page, which is the only place a decision ' +
1033
1038
  'becomes decided. A key the owner already decided is refused (DECISION_ALREADY_DECIDED). Value ' +
1034
- 'formats: amo.addonId {GUID} or name@domain; license an SPDX id such as MIT, or all-rights-reserved; ' +
1035
- 'seller.publicName text; seller.supportContact an email or HTTPS URL; seller.terms an HTTPS URL; ' +
1039
+ 'formats: amo.addonId {GUID} or name@domain; license an AMO license slug (below); seller.publicName ' +
1040
+ 'text; seller.supportContact an email or HTTPS URL; seller.terms an HTTPS URL; ' +
1036
1041
  'pricing {model: free|paid|donations, trialDays?, plans?: subscription|lifetime|one_time}; ' +
1037
- 'targetBrowsers an array of chrome/firefox/edge; targetLocales an array of locale codes. Idempotent ' +
1038
- 'per idempotencyKey. Requires project.write.',
1042
+ 'targetBrowsers an array of chrome/firefox/edge; targetLocales an array of locale codes. License: AMO ' +
1043
+ 'accepts exactly these case-sensitive slugs: all-rights-reserved, MIT, Apache-2.0, MPL-2.0, GPL-3.0-only, ' +
1044
+ 'ISC, BSD-2-Clause, GPL-2.0-only, LGPL-2.1-only, LGPL-3.0-only, AGPL-3.0-only, Unlicense. Another SPDX ' +
1045
+ 'id (BSD-3-Clause, GPL-3.0-or-later) is accepted, but AMO then needs it as customLicense (name + text) ' +
1046
+ "in the listing. npm's UNLICENSED in package.json means proprietary, so propose all-rights-reserved; " +
1047
+ 'Unlicense is a public-domain dedication. A misspelled slug (mit, GPL-3.0, UNLICENSED) is refused. ' +
1048
+ 'Idempotent per idempotencyKey. Requires project.write.',
1039
1049
  annotations: { readOnlyHint: false, idempotentHint: true, destructiveHint: false, openWorldHint: true },
1040
1050
  parameters: proposeDecisionSchema,
1041
1051
  execute: async (args, context) => {
@@ -1062,7 +1072,8 @@ export function registerTools(server, deps) {
1062
1072
  "When the project has a linked Pay application, the payment check also carries `platform`: what Extenshi's own " +
1063
1073
  'payment ledger proves (checkout, license, installation activation, recovery, refund). A live-mode purchase through ' +
1064
1074
  "those stages passes the check with source 'platform'; test-mode purchases are listed without passing it. Clients cannot " +
1065
- "record source 'platform'.",
1075
+ "record source 'platform'. `warnings` lists what does not block but needs action before upload, such as a " +
1076
+ 'decided license that is not an AMO slug and needs customLicense in the AMO listing.',
1066
1077
  'getReleaseReadiness',
1067
1078
  ],
1068
1079
  ];
@@ -1094,7 +1105,7 @@ export function registerTools(server, deps) {
1094
1105
  });
1095
1106
  add({
1096
1107
  name: 'connection_diagnostics',
1097
- 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.',
1108
+ description: 'Verify authentication, granted scopes, supported workspace contracts and actual write permissions (incl. hosted.write, evidence uploads and, for a projectId, CI ingest configuration) and the Pay scopes a key carries. Includes credit balances, the registered tools and, on the local server, `client` {version, latestVersion, updateAvailable}; credentials and secrets are not included. A failed sign-in returns an error that says how to sign in again.',
1098
1109
  annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
1099
1110
  parameters: z.object({}),
1100
1111
  execute: async (_args, context) => {
@@ -1104,7 +1115,13 @@ export function registerTools(server, deps) {
1104
1115
  client.connectionDiagnostics(),
1105
1116
  client.getApiCallerBalance(),
1106
1117
  ]);
1107
- return JSON.stringify({ connection, balance, registeredTools: registeredTools.map((t) => t.name) });
1118
+ const update = knownUpdate();
1119
+ return JSON.stringify({
1120
+ connection,
1121
+ balance,
1122
+ registeredTools: registeredTools.map((t) => t.name),
1123
+ ...(update ? { client: update } : {}),
1124
+ });
1108
1125
  }
1109
1126
  catch (err) {
1110
1127
  return readError(err, missingKeyMessage);
@@ -1524,7 +1541,7 @@ export function registerTools(server, deps) {
1524
1541
  'Built-in text (buttons, reasons, footers) is translated by Extenshi into en, es, pt-BR, fr, de, ' +
1525
1542
  'it, ru, ja, ko, zh-CN and ar and is not listed. A page is shown in the user’s browser language ' +
1526
1543
  'when the project offers it, otherwise in defaultLocale; an untranslated or outdated field shows ' +
1527
- 'its original. Requires project.read.',
1544
+ 'its original. Uninstall survey text itself is changed with update_uninstall_form. Requires project.read.',
1528
1545
  parameters: z.object({ projectId: z.string().uuid() }),
1529
1546
  execute: async (args, context) => {
1530
1547
  try {
@@ -1550,6 +1567,7 @@ export function registerTools(server, deps) {
1550
1567
  publicCode: z.string().max(40).describe('The page code, from get_page_translations.'),
1551
1568
  locale: z.string().max(35).describe('BCP 47 tag or the _locales spelling (de, pt-BR, pt_BR).'),
1552
1569
  translations: z.record(z.string().max(80), z.string().max(2000)),
1570
+ proposedBy: z.string().max(120).optional().describe('Your agent/tool name, shown to the owner.'),
1553
1571
  }),
1554
1572
  execute: async (args, context) => {
1555
1573
  try {
@@ -1621,6 +1639,8 @@ export function registerTools(server, deps) {
1621
1639
  });
1622
1640
  // get_license_terms_questionnaire / draft_license_terms (project.read, read-only).
1623
1641
  registerLicenseTermsTools(add, bff, (err) => readError(err, missingKeyMessage));
1642
+ // list/get/update/preview_uninstall_form + find_brand_mentions (project.read / hosted.write).
1643
+ registerUninstallFormTools(add, bff, (err) => readError(err, missingKeyMessage));
1624
1644
  // A local filesystem exists only where local artifacts do (stdio).
1625
1645
  const localFiles = caps.has('scan');
1626
1646
  add({
@@ -2043,7 +2063,7 @@ export function registerTools(server, deps) {
2043
2063
  add({
2044
2064
  name: 'get_credit_balance',
2045
2065
  description: 'Report your remaining Extenshi credits — for checking whether a large request fits the ' +
2046
- 'balance before running it. Returns every credit pool for your API key: `read` (spent one-per-call by ' +
2066
+ 'balance before running it. Returns every credit pool of the signed-in Extenshi account: `read` (spent one-per-call by ' +
2047
2067
  'search_extensions / get_extension / get_reviews / market_overview / get_risk_by_store_ids — the ' +
2048
2068
  'last one covers up to 40 extensions for that single credit; get_security costs 3) and ' +
2049
2069
  '`scan` (spent by scan_extension), plus `icon` and `inventory`. Each pool reports `remaining` — the ' +
@@ -2071,7 +2091,7 @@ export function registerTools(server, deps) {
2071
2091
  // data about other people's extensions, never for your own work.
2072
2092
  add({
2073
2093
  name: 'list_my_projects',
2074
- description: "List the extension projects owned by this API key — the developer's own workspaces on " +
2094
+ description: "List the extension projects owned by the signed-in Extenshi account — the developer's own workspaces on " +
2075
2095
  'extenshi.io. Returns id, name, status, target browsers, the bound GitHub repo (if any) and ' +
2076
2096
  'the claimed store listing (if any). Applies when the developer refers to "my extension" or ' +
2077
2097
  '"my project": the returned id is what get_project_state takes. FREE — reading your own ' +