@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/README.md +5 -0
- package/dist/bff.d.ts +32 -0
- package/dist/bff.d.ts.map +1 -1
- package/dist/bff.js +5 -0
- package/dist/bff.js.map +1 -1
- package/dist/development-guide.d.ts +18 -18
- package/dist/development-guide.d.ts.map +1 -1
- package/dist/development-guide.js +7 -2
- package/dist/development-guide.js.map +1 -1
- package/dist/landing-workflow.js +3 -3
- package/dist/landing-workflow.js.map +1 -1
- package/dist/mcp-analytics.d.ts.map +1 -1
- package/dist/mcp-analytics.js +56 -7
- package/dist/mcp-analytics.js.map +1 -1
- package/dist/publish.d.ts.map +1 -1
- package/dist/session-telemetry.d.ts +3 -0
- package/dist/session-telemetry.d.ts.map +1 -0
- package/dist/session-telemetry.js +13 -0
- package/dist/session-telemetry.js.map +1 -0
- package/dist/telemetry.d.ts +3 -1
- package/dist/telemetry.d.ts.map +1 -1
- package/dist/telemetry.js +7 -2
- package/dist/telemetry.js.map +1 -1
- package/dist/tools.d.ts +27 -2
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +81 -61
- package/dist/tools.js.map +1 -1
- package/dist/uninstall-form-tools.d.ts +120 -0
- package/dist/uninstall-form-tools.d.ts.map +1 -0
- package/dist/uninstall-form-tools.js +184 -0
- package/dist/uninstall-form-tools.js.map +1 -0
- package/dist/update-check.d.ts +9 -0
- package/dist/update-check.d.ts.map +1 -1
- package/dist/update-check.js +18 -1
- package/dist/update-check.js.map +1 -1
- package/package.json +2 -2
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
'
|
|
100
|
-
'
|
|
101
|
-
'
|
|
102
|
-
'
|
|
103
|
-
'
|
|
104
|
-
'
|
|
105
|
-
'
|
|
106
|
-
|
|
107
|
-
'
|
|
108
|
-
'
|
|
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;
|
|
119
|
-
:
|
|
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
|
-
*
|
|
225
|
-
*
|
|
226
|
-
* Local stdio
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
?
|
|
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
|
|
1035
|
-
'
|
|
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.
|
|
1038
|
-
'
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
|
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 ' +
|