@extenshi/mcp 0.0.1

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 ADDED
@@ -0,0 +1,67 @@
1
+ # @extenshi/mcp
2
+
3
+ A [Model Context Protocol](https://modelcontextprotocol.io) server that brings the
4
+ **Extenshi extension catalog** — search, security analysis, market research, and
5
+ pre-publish scanning — into your AI tools (Claude Code, Claude Desktop, Cursor, …).
6
+
7
+ It runs locally over **stdio**, so there's nothing to host. It reuses your existing
8
+ Extenshi API key (the same one `@extenshi/cli` uses) and talks to the public Extenshi
9
+ backend on your behalf.
10
+
11
+ ## Get an API key
12
+
13
+ An API key is **required** — every tool refuses to run without one, and the backend
14
+ enforces it too.
15
+
16
+ 1. Sign up at **https://auth.extenshi.io/signup**
17
+ 2. Create a key at **https://dojo.extenshi.io/api-keys**
18
+ 3. Provide it via the `EXTENSHI_API_KEY` environment variable (or run `extenshi login`,
19
+ which the MCP server reads from `~/.extenshi/config.json`).
20
+
21
+ ## Configure your MCP client
22
+
23
+ ```json
24
+ {
25
+ "mcpServers": {
26
+ "extenshi": {
27
+ "command": "npx",
28
+ "args": ["-y", "@extenshi/mcp"],
29
+ "env": { "EXTENSHI_API_KEY": "ek_…" }
30
+ }
31
+ }
32
+ }
33
+ ```
34
+
35
+ ## Tools
36
+
37
+ | Tool | What it does | Cost |
38
+ | --- | --- | --- |
39
+ | `search_extensions` | Hybrid search across Chrome/Firefox/Edge with filters (store, category, pricing, rating, risk, permissions) | free |
40
+ | `get_extension` | Full catalog detail for one extension by numeric catalog ID | free |
41
+ | `get_security` | Risk score + finding counts + top grouped findings (reads existing scans) | free |
42
+ | `get_related_extensions` | Similar / competing extensions for competitive analysis | free |
43
+ | `market_overview` | Aggregate catalog stats + facet counts (category / pricing / risk) | free |
44
+ | `scan_extension` | Pre-publish security scan of a local artifact (.zip/.crx/.xpi), with live progress | 1 credit (free for extensions you've verified ownership of) |
45
+
46
+ Read tools require a key but cost nothing. `scan_extension` spends one credit, or scans
47
+ free when you pass the numeric `extension_id` of an extension whose ownership you have
48
+ verified — top up at https://dojo.extenshi.io/billing.
49
+
50
+ ## Configuration
51
+
52
+ | Env var | Default | Purpose |
53
+ | --- | --- | --- |
54
+ | `EXTENSHI_API_KEY` | — | Your `ek_…` developer key (required) |
55
+ | `EXTENSHI_BFF_URL` | `https://bff.extenshi.io` | Catalog read API base URL |
56
+ | `EXTENSHI_API_URL` | `https://scan.extenshi.io` | Scan backend base URL |
57
+
58
+ ## Develop
59
+
60
+ ```bash
61
+ yarn build # tsc -> dist/
62
+ # Inspect locally:
63
+ EXTENSHI_API_KEY=ek_… npx @modelcontextprotocol/inspector node dist/index.js
64
+ ```
65
+
66
+ Publishing is handled by `./scripts/publish.sh` (npm Automation token from Infisical;
67
+ `DRY_RUN=1 ./scripts/publish.sh` to validate). Requires Node ≥20.
package/dist/bff.d.ts ADDED
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Thin tRPC client for the public catalog BFF (`bff.extenshi.io`).
3
+ *
4
+ * The BFF tRPC server is configured WITHOUT a data transformer (no superjson),
5
+ * so this client must match — plain JSON over the wire. We type the proxy as
6
+ * `any` on purpose: catalog-api emits no `.d.ts`, so the BFF↔API type chain
7
+ * can't be imported reliably across a published-package boundary. The runtime
8
+ * proxy builds the procedure path from property access regardless of types.
9
+ *
10
+ * Every call carries `Authorization: Bearer <ek_…>`. Today the BFF read
11
+ * procedures are anonymous, but they are moving to mandatory key-enforcement
12
+ * (no anonymous data access) — sending the key now is forward-compatible and
13
+ * needs no client change when enforcement lands.
14
+ */
15
+ export interface Bff {
16
+ searchExtensions(input: Record<string, unknown>): Promise<unknown>;
17
+ getExtensionById(id: number): Promise<unknown>;
18
+ getSecurityData(extensionId: number): Promise<unknown>;
19
+ getRiskSummary(extensionId: number): Promise<unknown>;
20
+ getRelatedExtensions(extensionId: number, limit: number): Promise<unknown>;
21
+ getSearchFacets(input: Record<string, unknown>): Promise<unknown>;
22
+ getStats(): Promise<unknown>;
23
+ }
24
+ export declare function makeBff(bffUrl: string, apiKey: string): Bff;
25
+ //# sourceMappingURL=bff.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bff.d.ts","sourceRoot":"","sources":["../src/bff.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAOH,MAAM,WAAW,GAAG;IACnB,gBAAgB,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;IAClE,gBAAgB,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;IAC9C,eAAe,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;IACtD,cAAc,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;IACrD,oBAAoB,CAAC,WAAW,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;IAC1E,eAAe,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;IACjE,QAAQ,IAAI,OAAO,CAAC,OAAO,CAAC,CAAA;CAC5B;AAED,wBAAgB,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,GAAG,CAsB3D"}
package/dist/bff.js ADDED
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Thin tRPC client for the public catalog BFF (`bff.extenshi.io`).
3
+ *
4
+ * The BFF tRPC server is configured WITHOUT a data transformer (no superjson),
5
+ * so this client must match — plain JSON over the wire. We type the proxy as
6
+ * `any` on purpose: catalog-api emits no `.d.ts`, so the BFF↔API type chain
7
+ * can't be imported reliably across a published-package boundary. The runtime
8
+ * proxy builds the procedure path from property access regardless of types.
9
+ *
10
+ * Every call carries `Authorization: Bearer <ek_…>`. Today the BFF read
11
+ * procedures are anonymous, but they are moving to mandatory key-enforcement
12
+ * (no anonymous data access) — sending the key now is forward-compatible and
13
+ * needs no client change when enforcement lands.
14
+ */
15
+ import { createTRPCClient, httpBatchLink } from '@trpc/client';
16
+ export function makeBff(bffUrl, apiKey) {
17
+ const client = createTRPCClient({
18
+ links: [
19
+ httpBatchLink({
20
+ url: `${bffUrl}/api/trpc`,
21
+ headers: () => ({ authorization: `Bearer ${apiKey}` }),
22
+ }),
23
+ ],
24
+ });
25
+ // NB: the store router is mounted under `catalog` in the BFF appRouter
26
+ // (routers/index.ts: `catalog: storeRouter`), NOT `store`. Security is `security`.
27
+ return {
28
+ searchExtensions: (input) => client.catalog.searchExtensions.query(input),
29
+ getExtensionById: (id) => client.catalog.getExtensionById.query({ id }),
30
+ getSecurityData: (extensionId) => client.security.getSecurityData.query({ extensionId }),
31
+ getRiskSummary: (extensionId) => client.security.getRiskSummary.query({ extensionId }),
32
+ getRelatedExtensions: (extensionId, limit) => client.catalog.getRelatedExtensions.query({ extensionId, limit }),
33
+ getSearchFacets: (input) => client.catalog.getSearchFacets.query(input),
34
+ getStats: () => client.catalog.getStats.query(),
35
+ };
36
+ }
37
+ //# sourceMappingURL=bff.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bff.js","sourceRoot":"","sources":["../src/bff.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,cAAc,CAAA;AAe9D,MAAM,UAAU,OAAO,CAAC,MAAc,EAAE,MAAc;IACrD,MAAM,MAAM,GAAkB,gBAAgB,CAAC;QAC9C,KAAK,EAAE;YACN,aAAa,CAAC;gBACb,GAAG,EAAE,GAAG,MAAM,WAAW;gBACzB,OAAO,EAAE,GAAG,EAAE,CAAC,CAAC,EAAE,aAAa,EAAE,UAAU,MAAM,EAAE,EAAE,CAAC;aACtD,CAAC;SACF;KACD,CAAC,CAAA;IAEF,uEAAuE;IACvE,mFAAmF;IACnF,OAAO;QACN,gBAAgB,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,MAAM,CAAC,OAAO,CAAC,gBAAgB,CAAC,KAAK,CAAC,KAAK,CAAC;QACzE,gBAAgB,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,MAAM,CAAC,OAAO,CAAC,gBAAgB,CAAC,KAAK,CAAC,EAAE,EAAE,EAAE,CAAC;QACvE,eAAe,EAAE,CAAC,WAAW,EAAE,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC,eAAe,CAAC,KAAK,CAAC,EAAE,WAAW,EAAE,CAAC;QACxF,cAAc,EAAE,CAAC,WAAW,EAAE,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC,cAAc,CAAC,KAAK,CAAC,EAAE,WAAW,EAAE,CAAC;QACtF,oBAAoB,EAAE,CAAC,WAAW,EAAE,KAAK,EAAE,EAAE,CAC5C,MAAM,CAAC,OAAO,CAAC,oBAAoB,CAAC,KAAK,CAAC,EAAE,WAAW,EAAE,KAAK,EAAE,CAAC;QAClE,eAAe,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,MAAM,CAAC,OAAO,CAAC,eAAe,CAAC,KAAK,CAAC,KAAK,CAAC;QACvE,QAAQ,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC,KAAK,EAAE;KAC/C,CAAA;AACF,CAAC"}
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Configuration loading for the Extenshi MCP server.
3
+ *
4
+ * The MCP server runs locally (stdio) inside the developer's AI client, so the
5
+ * API key comes from the environment — never from a request header. We
6
+ * deliberately reuse the SAME key sources as `@extenshi/cli` so a developer who
7
+ * already ran `extenshi login` is set up with zero extra steps:
8
+ *
9
+ * 1. Environment variable EXTENSHI_API_KEY (set in the client's mcp config)
10
+ * 2. A `.env` in the current working directory (does NOT override real env)
11
+ * 3. Config file ~/.extenshi/config.json (written by `extenshi login`)
12
+ *
13
+ * Base URLs default to production and are overridable for local/staging:
14
+ * - EXTENSHI_BFF_URL (read tools) default https://bff.extenshi.io
15
+ * - EXTENSHI_API_URL (scan) default https://scan.extenshi.io
16
+ *
17
+ * stdout is the MCP protocol channel — this module must NEVER write to it.
18
+ */
19
+ export interface ResolvedConfig {
20
+ /** The `ek_…` developer API key, or null if none is configured. */
21
+ apiKey: string | null;
22
+ /** Base URL of the catalog BFF (read tools), no trailing slash. */
23
+ bffUrl: string;
24
+ /** Base URL of the scan backend (scan_extension tool), no trailing slash. */
25
+ scanUrl: string;
26
+ }
27
+ export declare function loadConfig(): ResolvedConfig;
28
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAgBH,MAAM,WAAW,cAAc;IAC9B,mEAAmE;IACnE,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;IACrB,mEAAmE;IACnE,MAAM,EAAE,MAAM,CAAA;IACd,6EAA6E;IAC7E,OAAO,EAAE,MAAM,CAAA;CACf;AAoDD,wBAAgB,UAAU,IAAI,cAAc,CAS3C"}
package/dist/config.js ADDED
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Configuration loading for the Extenshi MCP server.
3
+ *
4
+ * The MCP server runs locally (stdio) inside the developer's AI client, so the
5
+ * API key comes from the environment — never from a request header. We
6
+ * deliberately reuse the SAME key sources as `@extenshi/cli` so a developer who
7
+ * already ran `extenshi login` is set up with zero extra steps:
8
+ *
9
+ * 1. Environment variable EXTENSHI_API_KEY (set in the client's mcp config)
10
+ * 2. A `.env` in the current working directory (does NOT override real env)
11
+ * 3. Config file ~/.extenshi/config.json (written by `extenshi login`)
12
+ *
13
+ * Base URLs default to production and are overridable for local/staging:
14
+ * - EXTENSHI_BFF_URL (read tools) default https://bff.extenshi.io
15
+ * - EXTENSHI_API_URL (scan) default https://scan.extenshi.io
16
+ *
17
+ * stdout is the MCP protocol channel — this module must NEVER write to it.
18
+ */
19
+ import fs from 'node:fs';
20
+ import os from 'node:os';
21
+ import path from 'node:path';
22
+ const DEFAULT_BFF_URL = 'https://bff.extenshi.io';
23
+ const DEFAULT_SCAN_URL = 'https://scan.extenshi.io';
24
+ const CONFIG_PATH = path.join(os.homedir(), '.extenshi', 'config.json');
25
+ function readConfigFile() {
26
+ try {
27
+ return JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8'));
28
+ }
29
+ catch {
30
+ return {};
31
+ }
32
+ }
33
+ /**
34
+ * Parse a `.env` body into key/value pairs. Supports comments (#), blank
35
+ * lines, an optional `export ` prefix, and single/double-quoted values.
36
+ */
37
+ function parseDotEnv(raw) {
38
+ const out = {};
39
+ for (const rawLine of raw.split(/\r?\n/)) {
40
+ const line = rawLine.trim();
41
+ if (!line || line.startsWith('#'))
42
+ continue;
43
+ const withoutExport = line.startsWith('export ') ? line.slice('export '.length) : line;
44
+ const eq = withoutExport.indexOf('=');
45
+ if (eq === -1)
46
+ continue;
47
+ const key = withoutExport.slice(0, eq).trim();
48
+ if (!key)
49
+ continue;
50
+ let value = withoutExport.slice(eq + 1).trim();
51
+ if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
52
+ value = value.slice(1, -1);
53
+ }
54
+ out[key] = value;
55
+ }
56
+ return out;
57
+ }
58
+ /** Load a local `.env` into process.env without overriding real env. Fail-soft. */
59
+ function loadDotEnv(cwd = process.cwd()) {
60
+ try {
61
+ const parsed = parseDotEnv(fs.readFileSync(path.join(cwd, '.env'), 'utf8'));
62
+ for (const [key, value] of Object.entries(parsed)) {
63
+ if (process.env[key] === undefined)
64
+ process.env[key] = value;
65
+ }
66
+ }
67
+ catch {
68
+ // No .env or unreadable — non-fatal.
69
+ }
70
+ }
71
+ function stripSlash(url) {
72
+ return url.replace(/\/$/, '');
73
+ }
74
+ export function loadConfig() {
75
+ loadDotEnv();
76
+ const file = readConfigFile();
77
+ return {
78
+ apiKey: process.env.EXTENSHI_API_KEY ?? file.apiKey ?? null,
79
+ bffUrl: stripSlash(process.env.EXTENSHI_BFF_URL ?? DEFAULT_BFF_URL),
80
+ scanUrl: stripSlash(process.env.EXTENSHI_API_URL ?? file.apiUrl ?? DEFAULT_SCAN_URL),
81
+ };
82
+ }
83
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * @extenshi/mcp — Model Context Protocol server for extension developers.
4
+ *
5
+ * Runs locally over stdio inside an MCP client (Claude Code/Desktop, Cursor)
6
+ * and exposes the Extenshi catalog as tools: market-research reads against the
7
+ * public BFF, plus a paid `scan_extension` that reuses the CLI scan path.
8
+ *
9
+ * Auth: an `ek_…` API key is REQUIRED. Over stdio there are no request headers,
10
+ * so the key comes from the environment (EXTENSHI_API_KEY) / `~/.extenshi`
11
+ * config — see config.ts. Every tool refuses to run without it and points the
12
+ * developer at the dedicated key page. The backend independently enforces the
13
+ * key, so this is defense in depth, not the only gate.
14
+ *
15
+ * NB: stdout is the MCP protocol channel — never write to it (use stderr).
16
+ */
17
+ export {};
18
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;;;;GAcG"}
package/dist/index.js ADDED
@@ -0,0 +1,262 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * @extenshi/mcp — Model Context Protocol server for extension developers.
4
+ *
5
+ * Runs locally over stdio inside an MCP client (Claude Code/Desktop, Cursor)
6
+ * and exposes the Extenshi catalog as tools: market-research reads against the
7
+ * public BFF, plus a paid `scan_extension` that reuses the CLI scan path.
8
+ *
9
+ * Auth: an `ek_…` API key is REQUIRED. Over stdio there are no request headers,
10
+ * so the key comes from the environment (EXTENSHI_API_KEY) / `~/.extenshi`
11
+ * config — see config.ts. Every tool refuses to run without it and points the
12
+ * developer at the dedicated key page. The backend independently enforces the
13
+ * key, so this is defense in depth, not the only gate.
14
+ *
15
+ * NB: stdout is the MCP protocol channel — never write to it (use stderr).
16
+ */
17
+ import { createRequire } from 'node:module';
18
+ import { FastMCP, UserError } from 'fastmcp';
19
+ import { z } from 'zod';
20
+ import { makeBff } from './bff.js';
21
+ import { loadConfig } from './config.js';
22
+ import { ScanError, scanArtifact } from './scan.js';
23
+ import { shapeExtension, shapeSearch, shapeSecurity } from './shape.js';
24
+ const require = createRequire(import.meta.url);
25
+ const pkg = require('../package.json');
26
+ const KEY_PAGE = 'https://dojo.extenshi.io/api-keys';
27
+ const SIGNUP_PAGE = 'https://auth.extenshi.io/signup';
28
+ const BILLING_PAGE = 'https://dojo.extenshi.io/billing';
29
+ const MISSING_KEY_MESSAGE = 'An Extenshi API key is required. Create one at ' +
30
+ `${KEY_PAGE} (sign up first at ${SIGNUP_PAGE} if needed), then set it as the ` +
31
+ 'EXTENSHI_API_KEY environment variable in your MCP client config (or run `extenshi login`).';
32
+ const cfg = loadConfig();
33
+ /** Return the configured key or refuse with an actionable instruction. */
34
+ function requireKey() {
35
+ if (!cfg.apiKey)
36
+ throw new UserError(MISSING_KEY_MESSAGE);
37
+ return cfg.apiKey;
38
+ }
39
+ /** Build a BFF client bound to the developer's key (verifies key presence). */
40
+ function bff() {
41
+ return makeBff(cfg.bffUrl, requireKey());
42
+ }
43
+ /** Normalize a thrown read error into a user-facing message. */
44
+ function readError(err) {
45
+ if (err instanceof UserError)
46
+ throw err;
47
+ const message = err instanceof Error ? err.message : String(err);
48
+ // A 401 means the BFF rejected the key (enforcement landed / key invalid).
49
+ if (/unauthorized|401|api key/i.test(message)) {
50
+ throw new UserError(`Request rejected — ${message}\n\n${MISSING_KEY_MESSAGE}`);
51
+ }
52
+ throw new UserError(message);
53
+ }
54
+ const server = new FastMCP({
55
+ name: 'extenshi',
56
+ // FastMCP types `version` as a semver template literal; package.json gives a plain string.
57
+ version: pkg.version,
58
+ instructions: 'Extenshi catalog tools for extension developers: search the cross-store catalog, ' +
59
+ 'inspect an extension and its security findings, find competitors, read market stats, ' +
60
+ 'and run a pre-publish security scan. Requires an Extenshi API key ' +
61
+ `(${KEY_PAGE}). Read tools are free; scan_extension spends 1 credit (free for ` +
62
+ 'extensions you have verified ownership of).',
63
+ });
64
+ // ── Read tools (free; key required) ─────────────────────────────────────────
65
+ server.addTool({
66
+ name: 'search_extensions',
67
+ description: 'Search the cross-store extension catalog (Chrome, Firefox, Edge) with hybrid relevance. ' +
68
+ 'Filter by store, category, pricing, rating, risk level, or requested permissions. ' +
69
+ 'Returns a compact list for market research and competitive analysis.',
70
+ parameters: z.object({
71
+ query: z.string().optional().describe('Free-text search query (name, description, keywords).'),
72
+ stores: z
73
+ .array(z.enum(['CHROME', 'FIREFOX', 'EDGE']))
74
+ .optional()
75
+ .describe('Limit to stores: CHROME, FIREFOX, and/or EDGE.'),
76
+ categories: z.array(z.string()).optional().describe('Catalog category slugs to include.'),
77
+ pricing: z
78
+ .array(z.enum(['FREE', 'FREEMIUM', 'IN_APP_PURCHASES', 'SUBSCRIPTION']))
79
+ .optional()
80
+ .describe('Pricing models to include.'),
81
+ risk: z
82
+ .array(z.enum(['NONE', 'LOW', 'MEDIUM', 'HIGH', 'CRITICAL']))
83
+ .optional()
84
+ .describe('Risk categories to include.'),
85
+ permissions: z.array(z.string()).optional().describe('Only extensions requesting these permissions.'),
86
+ minRating: z.number().min(0).max(5).optional(),
87
+ sortBy: z
88
+ .enum(['relevance', 'popular', 'rating', 'recent', 'name', 'safety'])
89
+ .optional()
90
+ .describe('Sort order (default: popular, or relevance when a query is given).'),
91
+ limit: z.number().int().min(1).max(25).default(20).describe('Max results to return (1–25).'),
92
+ }),
93
+ execute: async (args) => {
94
+ try {
95
+ const limit = args.limit ?? 20;
96
+ const result = await bff().searchExtensions({
97
+ query: args.query,
98
+ stores: args.stores,
99
+ categories: args.categories,
100
+ pricingModels: args.pricing,
101
+ riskCategories: args.risk,
102
+ permissions: args.permissions,
103
+ minRating: args.minRating,
104
+ sortBy: args.sortBy ?? (args.query ? 'relevance' : 'popular'),
105
+ take: limit,
106
+ });
107
+ return JSON.stringify(shapeSearch(result, limit), null, 2);
108
+ }
109
+ catch (err) {
110
+ return readError(err);
111
+ }
112
+ },
113
+ });
114
+ server.addTool({
115
+ name: 'get_extension',
116
+ description: 'Get full catalog detail for one extension by its numeric catalog ID: metadata, ' +
117
+ 'per-store ratings, install counts, categories, and a security badge.',
118
+ parameters: z.object({
119
+ extension_id: z.number().int().describe('Numeric catalog ID (from search_extensions results).'),
120
+ }),
121
+ execute: async (args) => {
122
+ try {
123
+ const result = await bff().getExtensionById(args.extension_id);
124
+ if (!result)
125
+ throw new UserError(`No extension found with catalog ID ${args.extension_id}.`);
126
+ return JSON.stringify(shapeExtension(result), null, 2);
127
+ }
128
+ catch (err) {
129
+ return readError(err);
130
+ }
131
+ },
132
+ });
133
+ server.addTool({
134
+ name: 'get_security',
135
+ description: 'Get the security analysis for an extension: risk score/category, finding counts by ' +
136
+ 'severity, and the top grouped findings (scanner, rule, severity, count). ' +
137
+ 'Reads existing scan results — does not trigger a new scan.',
138
+ parameters: z.object({
139
+ extension_id: z.number().int().describe('Numeric catalog ID.'),
140
+ }),
141
+ execute: async (args) => {
142
+ try {
143
+ const client = bff();
144
+ const [security, riskSummary] = await Promise.all([
145
+ client.getSecurityData(args.extension_id).catch(() => null),
146
+ client.getRiskSummary(args.extension_id).catch(() => null),
147
+ ]);
148
+ if (!security && !riskSummary) {
149
+ return JSON.stringify({ scanned: false, message: 'This extension has not been scanned yet.' }, null, 2);
150
+ }
151
+ return JSON.stringify(shapeSecurity(security, riskSummary), null, 2);
152
+ }
153
+ catch (err) {
154
+ return readError(err);
155
+ }
156
+ },
157
+ });
158
+ server.addTool({
159
+ name: 'get_related_extensions',
160
+ description: 'Find similar/competing extensions for a given extension — useful for competitive ' +
161
+ 'analysis and alternatives research.',
162
+ parameters: z.object({
163
+ extension_id: z.number().int().describe('Numeric catalog ID.'),
164
+ limit: z.number().int().min(1).max(12).default(6).describe('Max alternatives to return (1–12).'),
165
+ }),
166
+ execute: async (args) => {
167
+ try {
168
+ const result = await bff().getRelatedExtensions(args.extension_id, args.limit ?? 6);
169
+ return JSON.stringify(shapeSearch(result, args.limit ?? 6), null, 2);
170
+ }
171
+ catch (err) {
172
+ return readError(err);
173
+ }
174
+ },
175
+ });
176
+ server.addTool({
177
+ name: 'market_overview',
178
+ description: 'Get aggregate catalog market stats and facet counts (by category, pricing model, and ' +
179
+ 'risk level). Pass an optional query/filters to scope the facets to a search.',
180
+ parameters: z.object({
181
+ query: z.string().optional().describe('Optional query to scope facets to a search.'),
182
+ stores: z.array(z.enum(['CHROME', 'FIREFOX', 'EDGE'])).optional(),
183
+ categories: z.array(z.string()).optional(),
184
+ }),
185
+ execute: async (args) => {
186
+ try {
187
+ const client = bff();
188
+ const [stats, facets] = await Promise.all([
189
+ client.getStats().catch(() => null),
190
+ client
191
+ .getSearchFacets({ query: args.query, stores: args.stores, categories: args.categories })
192
+ .catch(() => null),
193
+ ]);
194
+ return JSON.stringify({
195
+ stats: stats ?? undefined,
196
+ facets: facets ?? undefined,
197
+ }, null, 2);
198
+ }
199
+ catch (err) {
200
+ return readError(err);
201
+ }
202
+ },
203
+ });
204
+ // ── Action tool (paid; key required) ────────────────────────────────────────
205
+ server.addTool({
206
+ name: 'scan_extension',
207
+ description: 'Run a pre-publish security scan on a local extension artifact (.zip/.crx/.xpi) and ' +
208
+ 'return the report. Spends 1 credit — FREE for an extension whose ownership you have ' +
209
+ 'verified (pass its numeric catalog extension_id). Streams live per-scanner progress.',
210
+ parameters: z.object({
211
+ artifact_path: z.string().describe('Absolute or relative path to the built extension artifact (≤50 MB).'),
212
+ extension_id: z
213
+ .number()
214
+ .int()
215
+ .optional()
216
+ .describe('Numeric catalog ID of a VERIFIED-owned extension — required to spend a FREE credit.'),
217
+ }),
218
+ execute: async (args, { reportProgress }) => {
219
+ const apiKey = requireKey();
220
+ try {
221
+ const report = await scanArtifact({
222
+ artifactPath: args.artifact_path,
223
+ apiKey,
224
+ scanUrl: cfg.scanUrl,
225
+ extensionId: args.extension_id?.toString(),
226
+ onProgress: (p) => {
227
+ if (typeof p.total === 'number' && p.total > 0) {
228
+ void reportProgress({ progress: p.completed ?? 0, total: p.total });
229
+ }
230
+ },
231
+ });
232
+ return JSON.stringify(shapeExtension(report), null, 2);
233
+ }
234
+ catch (err) {
235
+ if (err instanceof ScanError)
236
+ throw new UserError(scanErrorMessage(err));
237
+ throw err;
238
+ }
239
+ },
240
+ });
241
+ /** Map a scan backend failure to an actionable next step. */
242
+ function scanErrorMessage(err) {
243
+ switch (err.status) {
244
+ case 401:
245
+ return `Authentication failed: ${err.message}\n\n${MISSING_KEY_MESSAGE}`;
246
+ case 402:
247
+ return `Out of credits. Top up at ${BILLING_PAGE}.`;
248
+ case 403:
249
+ if (err.errorCode === 'FREE_REQUIRES_CLAIM' || err.errorCode === 'FREE_REQUIRES_VERIFIED_OWNERSHIP') {
250
+ return (`${err.message}\n\nFree credits only cover extensions you have verified ownership of. ` +
251
+ 'Pass the numeric catalog extension_id of a verified extension, or buy credits at ' +
252
+ `${BILLING_PAGE} to scan arbitrary artifacts.`);
253
+ }
254
+ return `Access denied: ${err.message}`;
255
+ case 429:
256
+ return `Rate limited — wait ${err.retryAfterSec ?? 60}s before trying again.`;
257
+ default:
258
+ return err.message;
259
+ }
260
+ }
261
+ server.start({ transportType: 'stdio' });
262
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/scan.d.ts ADDED
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Scan an extension artifact against the Extenshi scan backend and RETURN the
3
+ * report (no printing) — the MCP-friendly counterpart to the CLI's `runScan`.
4
+ *
5
+ * It replicates the exact request `@extenshi/cli` makes
6
+ * (`POST {scanUrl}/api/v1/scan`, multipart `apiKey` + `artifact` + optional
7
+ * `extensionId`, SSE progress, 240s timeout, server-side credit
8
+ * consume→commit→refund) so the MCP `scan_extension` tool and the CLI bill and
9
+ * behave identically. The network + SSE logic is vendored (not imported) to
10
+ * keep this package independently publishable — npm rejects `workspace:*`.
11
+ *
12
+ * Errors surface as `ScanError` carrying the HTTP status + machine `errorCode`
13
+ * so the caller can render the right next-step instruction.
14
+ */
15
+ export interface ScanProgress {
16
+ stage?: string;
17
+ scanner?: string | null;
18
+ completed?: number;
19
+ total?: number | null;
20
+ pct?: number;
21
+ }
22
+ export interface ScanArtifactOptions {
23
+ artifactPath: string;
24
+ apiKey: string;
25
+ /** Scan backend base URL, no trailing slash (e.g. https://scan.extenshi.io). */
26
+ scanUrl: string;
27
+ /** Numeric catalog ID — required to spend a FREE credit on an unverified extension. */
28
+ extensionId?: string;
29
+ /** Called for each upstream progress event so the caller can stream it. */
30
+ onProgress?: (p: ScanProgress) => void;
31
+ }
32
+ /** A scan failure that carries the HTTP status and the backend's machine code. */
33
+ export declare class ScanError extends Error {
34
+ status?: number;
35
+ errorCode?: string;
36
+ retryAfterSec?: number;
37
+ constructor(message: string, opts?: {
38
+ status?: number;
39
+ errorCode?: string;
40
+ retryAfterSec?: number;
41
+ });
42
+ }
43
+ export type ScanReport = Record<string, unknown>;
44
+ export declare function scanArtifact(opts: ScanArtifactOptions): Promise<ScanReport>;
45
+ //# sourceMappingURL=scan.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scan.d.ts","sourceRoot":"","sources":["../src/scan.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAUH,MAAM,WAAW,YAAY;IAC5B,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACrB,GAAG,CAAC,EAAE,MAAM,CAAA;CACZ;AAED,MAAM,WAAW,mBAAmB;IACnC,YAAY,EAAE,MAAM,CAAA;IACpB,MAAM,EAAE,MAAM,CAAA;IACd,gFAAgF;IAChF,OAAO,EAAE,MAAM,CAAA;IACf,uFAAuF;IACvF,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,2EAA2E;IAC3E,UAAU,CAAC,EAAE,CAAC,CAAC,EAAE,YAAY,KAAK,IAAI,CAAA;CACtC;AAED,kFAAkF;AAClF,qBAAa,SAAU,SAAQ,KAAK;IACnC,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,aAAa,CAAC,EAAE,MAAM,CAAA;gBAEV,OAAO,EAAE,MAAM,EAAE,IAAI,GAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAC;QAAC,aAAa,CAAC,EAAE,MAAM,CAAA;KAAO;CAOvG;AAED,MAAM,MAAM,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;AAEhD,wBAAsB,YAAY,CAAC,IAAI,EAAE,mBAAmB,GAAG,OAAO,CAAC,UAAU,CAAC,CAiDjF"}
package/dist/scan.js ADDED
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Scan an extension artifact against the Extenshi scan backend and RETURN the
3
+ * report (no printing) — the MCP-friendly counterpart to the CLI's `runScan`.
4
+ *
5
+ * It replicates the exact request `@extenshi/cli` makes
6
+ * (`POST {scanUrl}/api/v1/scan`, multipart `apiKey` + `artifact` + optional
7
+ * `extensionId`, SSE progress, 240s timeout, server-side credit
8
+ * consume→commit→refund) so the MCP `scan_extension` tool and the CLI bill and
9
+ * behave identically. The network + SSE logic is vendored (not imported) to
10
+ * keep this package independently publishable — npm rejects `workspace:*`.
11
+ *
12
+ * Errors surface as `ScanError` carrying the HTTP status + machine `errorCode`
13
+ * so the caller can render the right next-step instruction.
14
+ */
15
+ import fs from 'node:fs';
16
+ import path from 'node:path';
17
+ import { FormData, fetch } from 'undici';
18
+ import { parseSseFrames } from './sse.js';
19
+ const SCAN_TIMEOUT_MS = 240_000;
20
+ const MAX_ARTIFACT_BYTES = 50 * 1024 * 1024;
21
+ /** A scan failure that carries the HTTP status and the backend's machine code. */
22
+ export class ScanError extends Error {
23
+ status;
24
+ errorCode;
25
+ retryAfterSec;
26
+ constructor(message, opts = {}) {
27
+ super(message);
28
+ this.name = 'ScanError';
29
+ this.status = opts.status;
30
+ this.errorCode = opts.errorCode;
31
+ this.retryAfterSec = opts.retryAfterSec;
32
+ }
33
+ }
34
+ export async function scanArtifact(opts) {
35
+ const { artifactPath, apiKey, scanUrl, extensionId, onProgress } = opts;
36
+ if (!fs.existsSync(artifactPath)) {
37
+ throw new ScanError(`Artifact file not found: ${artifactPath}`);
38
+ }
39
+ const stats = fs.statSync(artifactPath);
40
+ if (!stats.isFile()) {
41
+ throw new ScanError(`Artifact path is not a file: ${artifactPath}`);
42
+ }
43
+ if (stats.size > MAX_ARTIFACT_BYTES) {
44
+ throw new ScanError(`Artifact exceeds 50 MB limit (${(stats.size / 1024 / 1024).toFixed(1)} MB)`);
45
+ }
46
+ const form = new FormData();
47
+ form.append('apiKey', apiKey);
48
+ form.append('artifact', new File([fs.readFileSync(artifactPath)], path.basename(artifactPath)));
49
+ if (extensionId)
50
+ form.append('extensionId', extensionId);
51
+ let response;
52
+ try {
53
+ response = await fetch(`${scanUrl}/api/v1/scan`, {
54
+ method: 'POST',
55
+ body: form,
56
+ headers: { accept: 'text/event-stream' },
57
+ signal: AbortSignal.timeout(SCAN_TIMEOUT_MS),
58
+ });
59
+ }
60
+ catch (err) {
61
+ if (err instanceof Error && err.name === 'TimeoutError') {
62
+ throw new ScanError(`Scan timed out after ${SCAN_TIMEOUT_MS / 1000}s. If a credit was charged it auto-refunds within 1 minute — do NOT retry immediately.`);
63
+ }
64
+ throw err;
65
+ }
66
+ // Auth/billing/gateway errors are always JSON and arrive before any stream.
67
+ if (!response.ok) {
68
+ const body = await parseJsonBody(response);
69
+ throw errorForStatus(response.status, body);
70
+ }
71
+ const contentType = response.headers.get('content-type') ?? '';
72
+ if (contentType.includes('text/event-stream')) {
73
+ return await consumeSseStream(response, onProgress);
74
+ }
75
+ // Legacy single-JSON mode (server ignored the Accept header).
76
+ return await parseJsonBody(response);
77
+ }
78
+ async function consumeSseStream(response, onProgress) {
79
+ if (!response.body)
80
+ throw new ScanError('Scan stream had no response body');
81
+ const decoder = new TextDecoder();
82
+ let buffer = '';
83
+ let report = null;
84
+ let errorMessage = null;
85
+ const handle = (ev) => {
86
+ if (ev.event === 'progress') {
87
+ const p = safeJson(ev.data);
88
+ if (p && onProgress)
89
+ onProgress(p);
90
+ }
91
+ else if (ev.event === 'result') {
92
+ report = safeJson(ev.data);
93
+ }
94
+ else if (ev.event === 'error') {
95
+ const e = safeJson(ev.data);
96
+ errorMessage = e && typeof e.error === 'string' ? e.error : 'scan failed';
97
+ }
98
+ };
99
+ for await (const chunk of response.body) {
100
+ buffer += decoder.decode(chunk, { stream: true });
101
+ const { events, rest } = parseSseFrames(buffer);
102
+ buffer = rest;
103
+ for (const ev of events)
104
+ handle(ev);
105
+ }
106
+ // Flush a trailing frame not terminated by a blank line.
107
+ const { events } = parseSseFrames(`${buffer}\n\n`);
108
+ for (const ev of events)
109
+ handle(ev);
110
+ if (errorMessage)
111
+ throw new ScanError(`Scan failed: ${errorMessage}`);
112
+ if (!report)
113
+ throw new ScanError('Scan stream ended before returning a result');
114
+ return report;
115
+ }
116
+ function safeJson(data) {
117
+ try {
118
+ return JSON.parse(data);
119
+ }
120
+ catch {
121
+ return null;
122
+ }
123
+ }
124
+ async function parseJsonBody(response) {
125
+ const raw = await response.text();
126
+ try {
127
+ return JSON.parse(raw);
128
+ }
129
+ catch {
130
+ const snippet = raw.replace(/\s+/g, ' ').trim().slice(0, 200);
131
+ throw new ScanError(`Scan backend returned a non-JSON response (HTTP ${response.status}). ${snippet ? `Response: ${snippet}` : ''}`.trim(), { status: response.status });
132
+ }
133
+ }
134
+ function errorForStatus(status, body) {
135
+ const message = typeof body.error === 'string' ? body.error : `HTTP ${status}`;
136
+ const errorCode = typeof body.errorCode === 'string' ? body.errorCode : undefined;
137
+ const retryAfterSec = typeof body.retryAfterSec === 'number'
138
+ ? body.retryAfterSec
139
+ : undefined;
140
+ return new ScanError(message, { status, errorCode, retryAfterSec });
141
+ }
142
+ //# sourceMappingURL=scan.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scan.js","sourceRoot":"","sources":["../src/scan.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Output shaping for MCP tool results.
3
+ *
4
+ * MCP tool results land directly in the AI client's context window, so we keep
5
+ * them compact and curated. Field names below match the live BFF payloads
6
+ * (catalog router mounted as `catalog`, security as `security`):
7
+ * - search items wrap a cross-store cluster: `{ id, slug, snapshots[], availableStores, security }`
8
+ * where the human-facing fields (name, rating, users, store) live on each snapshot.
9
+ * - `getExtensionById` returns the cluster with aggregated `latest*` fields.
10
+ * - `getSecurityData` returns `{ riskAssessment, scanExecution, findings:{ total, groupTotal, bySeverity } }`.
11
+ * - `getRiskSummary` is already a compact summary (or null if never scanned).
12
+ *
13
+ * `compact()` is a defensive fallback that bounds array length / string length /
14
+ * depth so a tool can never blow up the context even if a shape drifts.
15
+ */
16
+ interface CompactOpts {
17
+ maxArray?: number;
18
+ maxString?: number;
19
+ maxDepth?: number;
20
+ }
21
+ /** Recursively bound a value's size: cap arrays, truncate strings, limit depth. */
22
+ export declare function compact(value: unknown, opts?: CompactOpts, depth?: number): unknown;
23
+ type Obj = Record<string, unknown>;
24
+ /** Compact a search / related result down to a count + curated items. */
25
+ export declare function shapeSearch(result: unknown, limit: number): Obj;
26
+ /** Curate a single extension detail (`getExtensionById`). */
27
+ export declare function shapeExtension(result: unknown): unknown;
28
+ /** Curate the security view: risk summary + grouped findings (top N per severity). */
29
+ export declare function shapeSecurity(security: unknown, riskSummary: unknown): Obj;
30
+ export {};
31
+ //# sourceMappingURL=shape.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shape.d.ts","sourceRoot":"","sources":["../src/shape.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,UAAU,WAAW;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAA;CACjB;AAED,mFAAmF;AACnF,wBAAgB,OAAO,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,GAAE,WAAgB,EAAE,KAAK,SAAI,GAAG,OAAO,CAqBlF;AAED,KAAK,GAAG,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;AA+ClC,yEAAyE;AACzE,wBAAgB,WAAW,CAAC,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,GAAG,GAAG,CAS/D;AAED,6DAA6D;AAC7D,wBAAgB,cAAc,CAAC,MAAM,EAAE,OAAO,GAAG,OAAO,CAiCvD;AAkBD,sFAAsF;AACtF,wBAAgB,aAAa,CAAC,QAAQ,EAAE,OAAO,EAAE,WAAW,EAAE,OAAO,GAAG,GAAG,CAuC1E"}
package/dist/shape.js ADDED
@@ -0,0 +1,184 @@
1
+ /**
2
+ * Output shaping for MCP tool results.
3
+ *
4
+ * MCP tool results land directly in the AI client's context window, so we keep
5
+ * them compact and curated. Field names below match the live BFF payloads
6
+ * (catalog router mounted as `catalog`, security as `security`):
7
+ * - search items wrap a cross-store cluster: `{ id, slug, snapshots[], availableStores, security }`
8
+ * where the human-facing fields (name, rating, users, store) live on each snapshot.
9
+ * - `getExtensionById` returns the cluster with aggregated `latest*` fields.
10
+ * - `getSecurityData` returns `{ riskAssessment, scanExecution, findings:{ total, groupTotal, bySeverity } }`.
11
+ * - `getRiskSummary` is already a compact summary (or null if never scanned).
12
+ *
13
+ * `compact()` is a defensive fallback that bounds array length / string length /
14
+ * depth so a tool can never blow up the context even if a shape drifts.
15
+ */
16
+ /** Recursively bound a value's size: cap arrays, truncate strings, limit depth. */
17
+ export function compact(value, opts = {}, depth = 0) {
18
+ const { maxArray = 20, maxString = 400, maxDepth = 5 } = opts;
19
+ if (typeof value === 'string') {
20
+ return value.length > maxString ? `${value.slice(0, maxString)}…` : value;
21
+ }
22
+ if (value === null || typeof value !== 'object')
23
+ return value;
24
+ if (depth >= maxDepth)
25
+ return Array.isArray(value) ? `[${value.length} items]` : '{…}';
26
+ if (Array.isArray(value)) {
27
+ const capped = value.slice(0, maxArray).map((v) => compact(v, opts, depth + 1));
28
+ if (value.length > maxArray)
29
+ capped.push(`…+${value.length - maxArray} more`);
30
+ return capped;
31
+ }
32
+ const out = {};
33
+ for (const [k, v] of Object.entries(value)) {
34
+ if (v === undefined || v === null)
35
+ continue;
36
+ out[k] = compact(v, opts, depth + 1);
37
+ }
38
+ return out;
39
+ }
40
+ function isObj(v) {
41
+ return !!v && typeof v === 'object' && !Array.isArray(v);
42
+ }
43
+ /** Drop nullish entries so curated objects stay terse. */
44
+ function prune(obj) {
45
+ for (const k of Object.keys(obj))
46
+ if (obj[k] === undefined || obj[k] === null)
47
+ delete obj[k];
48
+ return obj;
49
+ }
50
+ function primaryCategory(snap) {
51
+ const cats = snap.categories;
52
+ if (Array.isArray(cats) && isObj(cats[0]))
53
+ return cats[0].name ?? cats[0].displayName;
54
+ return undefined;
55
+ }
56
+ /** Curate one search-result cluster via its primary (first) snapshot. */
57
+ function shapeSearchItem(item) {
58
+ if (!isObj(item))
59
+ return { value: compact(item) };
60
+ const snaps = Array.isArray(item.snapshots) ? item.snapshots : [];
61
+ const snap = isObj(snaps[0]) ? snaps[0] : {};
62
+ const security = isObj(item.security) ? item.security : undefined;
63
+ const riskAssessment = security && isObj(security.riskAssessment) ? security.riskAssessment : undefined;
64
+ return prune({
65
+ id: item.id,
66
+ slug: item.slug,
67
+ name: snap.name,
68
+ author: snap.authorName,
69
+ stores: item.availableStores,
70
+ store: snap.store,
71
+ storeId: snap.storeId,
72
+ users: snap.usersNumeric ?? snap.weeklyDownloads,
73
+ rating: snap.rating,
74
+ reviews: snap.ratingCount,
75
+ category: primaryCategory(snap),
76
+ version: snap.version,
77
+ lastUpdated: snap.lastUpdated,
78
+ sizeBytes: item.latestSizeBytes,
79
+ riskScore: riskAssessment?.overallScore ?? security?.overallScore,
80
+ shortDescription: typeof snap.shortDescription === 'string' ? snap.shortDescription.slice(0, 200) : undefined,
81
+ });
82
+ }
83
+ /** Compact a search / related result down to a count + curated items. */
84
+ export function shapeSearch(result, limit) {
85
+ const arr = Array.isArray(result)
86
+ ? result
87
+ : isObj(result) && Array.isArray(result.items)
88
+ ? result.items
89
+ : [];
90
+ const items = arr.slice(0, limit).map(shapeSearchItem);
91
+ const total = isObj(result) ? (result.total ?? result.totalCount) : undefined;
92
+ return prune({ count: items.length, total, items });
93
+ }
94
+ /** Curate a single extension detail (`getExtensionById`). */
95
+ export function shapeExtension(result) {
96
+ if (!isObj(result))
97
+ return result ?? null;
98
+ const snaps = Array.isArray(result.snapshots) ? result.snapshots : [];
99
+ const stores = [...new Set(snaps.map((s) => (isObj(s) ? s.store : undefined)).filter(Boolean))];
100
+ const reviews = result.reviews;
101
+ return prune({
102
+ id: result.id,
103
+ slug: result.slug,
104
+ name: result.latestName,
105
+ rating: result.latestRating,
106
+ users: result.latestUsersNumeric,
107
+ lastUpdated: result.latestUpdatedAt,
108
+ traderStatus: result.latestTraderStatus,
109
+ riskScore: result.latestRiskScore,
110
+ riskScoreAt: result.latestRiskScoreAt,
111
+ sizeBytes: result.latestSizeBytes,
112
+ hidden: result.hidden || undefined,
113
+ stores: stores.length ? stores : undefined,
114
+ reviewCount: Array.isArray(reviews) ? reviews.length : reviews,
115
+ snapshots: snaps.slice(0, 6).map((s) => prune({
116
+ store: s.store,
117
+ storeId: s.storeId,
118
+ name: s.name,
119
+ rating: s.rating,
120
+ reviews: s.ratingCount,
121
+ users: s.usersNumeric ?? s.weeklyDownloads,
122
+ version: s.version,
123
+ lastUpdated: s.lastUpdated,
124
+ category: primaryCategory(s),
125
+ })),
126
+ });
127
+ }
128
+ /** Curate a finding group to the essentials. */
129
+ function shapeFinding(f) {
130
+ if (!isObj(f))
131
+ return { value: compact(f) };
132
+ const locations = Array.isArray(f.locations)
133
+ ? f.locations.slice(0, 5).map((l) => (isObj(l) ? l.file : l))
134
+ : undefined;
135
+ return prune({
136
+ scanner: f.scanner,
137
+ rule: f.rule_id ?? f.ruleId,
138
+ severity: f.severity,
139
+ title: f.title,
140
+ count: f.count,
141
+ files: locations,
142
+ });
143
+ }
144
+ /** Curate the security view: risk summary + grouped findings (top N per severity). */
145
+ export function shapeSecurity(security, riskSummary) {
146
+ const out = {};
147
+ if (isObj(riskSummary)) {
148
+ out.summary = riskSummary;
149
+ }
150
+ else if (isObj(security) && isObj(security.riskAssessment)) {
151
+ const ra = security.riskAssessment;
152
+ out.summary = prune({
153
+ overallScore: ra.overallScore,
154
+ riskCategory: ra.riskCategory,
155
+ severityBreakdown: isObj(ra.contributingFactors)
156
+ ? ra.contributingFactors.severity_breakdown
157
+ : undefined,
158
+ });
159
+ }
160
+ if (isObj(security)) {
161
+ if (isObj(security.scanExecution)) {
162
+ const se = security.scanExecution;
163
+ out.scan = prune({ status: se.status, completedAt: se.completedAt });
164
+ }
165
+ if (isObj(security.findings)) {
166
+ const f = security.findings;
167
+ const bySeverity = {};
168
+ if (isObj(f.bySeverity)) {
169
+ for (const [sev, rows] of Object.entries(f.bySeverity)) {
170
+ if (Array.isArray(rows) && rows.length) {
171
+ bySeverity[sev] = rows.slice(0, 8).map(shapeFinding);
172
+ if (rows.length > 8)
173
+ bySeverity[sev].push(`…+${rows.length - 8} more`);
174
+ }
175
+ }
176
+ }
177
+ out.findings = prune({ total: f.total, groupTotal: f.groupTotal, bySeverity });
178
+ }
179
+ }
180
+ if (Object.keys(out).length === 0)
181
+ return { scanned: false, message: 'This extension has not been scanned yet.' };
182
+ return out;
183
+ }
184
+ //# sourceMappingURL=shape.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shape.js","sourceRoot":"","sources":["../src/shape.ts"],"names":[],"mappings":"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"}
package/dist/sse.d.ts ADDED
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Minimal Server-Sent Events frame parser for the scan progress stream.
3
+ *
4
+ * Vendored from `@extenshi/cli` (tools/extenshi-cli/src/sse.ts) so this package
5
+ * stays a clean standalone npm publish with no cross-workspace dependency
6
+ * (npm rejects `workspace:*` at publish time — see scripts/publish.sh in the
7
+ * CLI). The scan backend emits `text/event-stream` with named events
8
+ * (`progress`, `result`, `error`) plus keep-alive comment frames (`:ping`).
9
+ *
10
+ * Pure (no I/O): feed it the accumulated buffer, get back the complete events
11
+ * plus the unconsumed tail to carry into the next chunk.
12
+ */
13
+ export interface SseEvent {
14
+ /** The `event:` field value (defaults to "message" per the SSE spec). */
15
+ event: string;
16
+ /** The joined `data:` field value(s). */
17
+ data: string;
18
+ }
19
+ /**
20
+ * Split an accumulated SSE buffer into complete events. Frames are delimited by
21
+ * a blank line (`\n\n`); a partial trailing frame is returned as `rest`.
22
+ * Comment frames (lines starting with `:`) and dataless frames are skipped.
23
+ */
24
+ export declare function parseSseFrames(buffer: string): {
25
+ events: SseEvent[];
26
+ rest: string;
27
+ };
28
+ //# sourceMappingURL=sse.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sse.d.ts","sourceRoot":"","sources":["../src/sse.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,MAAM,WAAW,QAAQ;IACxB,yEAAyE;IACzE,KAAK,EAAE,MAAM,CAAA;IACb,yCAAyC;IACzC,IAAI,EAAE,MAAM,CAAA;CACZ;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG;IAAE,MAAM,EAAE,QAAQ,EAAE,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAgCnF"}
package/dist/sse.js ADDED
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Minimal Server-Sent Events frame parser for the scan progress stream.
3
+ *
4
+ * Vendored from `@extenshi/cli` (tools/extenshi-cli/src/sse.ts) so this package
5
+ * stays a clean standalone npm publish with no cross-workspace dependency
6
+ * (npm rejects `workspace:*` at publish time — see scripts/publish.sh in the
7
+ * CLI). The scan backend emits `text/event-stream` with named events
8
+ * (`progress`, `result`, `error`) plus keep-alive comment frames (`:ping`).
9
+ *
10
+ * Pure (no I/O): feed it the accumulated buffer, get back the complete events
11
+ * plus the unconsumed tail to carry into the next chunk.
12
+ */
13
+ /**
14
+ * Split an accumulated SSE buffer into complete events. Frames are delimited by
15
+ * a blank line (`\n\n`); a partial trailing frame is returned as `rest`.
16
+ * Comment frames (lines starting with `:`) and dataless frames are skipped.
17
+ */
18
+ export function parseSseFrames(buffer) {
19
+ let buf = buffer.replace(/\r\n/g, '\n');
20
+ const events = [];
21
+ while (true) {
22
+ const boundary = buf.indexOf('\n\n');
23
+ if (boundary === -1)
24
+ break;
25
+ const frame = buf.slice(0, boundary);
26
+ buf = buf.slice(boundary + 2);
27
+ let eventName = 'message';
28
+ const dataLines = [];
29
+ for (const line of frame.split('\n')) {
30
+ if (line === '' || line.startsWith(':'))
31
+ continue; // blank or comment (keep-alive)
32
+ const colon = line.indexOf(':');
33
+ const field = colon === -1 ? line : line.slice(0, colon);
34
+ let value = colon === -1 ? '' : line.slice(colon + 1);
35
+ if (value.startsWith(' '))
36
+ value = value.slice(1);
37
+ if (field === 'event')
38
+ eventName = value;
39
+ else if (field === 'data')
40
+ dataLines.push(value);
41
+ }
42
+ if (dataLines.length > 0) {
43
+ events.push({ event: eventName, data: dataLines.join('\n') });
44
+ }
45
+ }
46
+ return { events, rest: buf };
47
+ }
48
+ //# sourceMappingURL=sse.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sse.js","sourceRoot":"","sources":["../src/sse.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AASH;;;;GAIG;AACH,MAAM,UAAU,cAAc,CAAC,MAAc;IAC5C,IAAI,GAAG,GAAG,MAAM,CAAC,OAAO,CAAC,OAAO,EAAE,IAAI,CAAC,CAAA;IACvC,MAAM,MAAM,GAAe,EAAE,CAAA;IAE7B,OAAO,IAAI,EAAE,CAAC;QACb,MAAM,QAAQ,GAAG,GAAG,CAAC,OAAO,CAAC,MAAM,CAAC,CAAA;QACpC,IAAI,QAAQ,KAAK,CAAC,CAAC;YAAE,MAAK;QAE1B,MAAM,KAAK,GAAG,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAA;QACpC,GAAG,GAAG,GAAG,CAAC,KAAK,CAAC,QAAQ,GAAG,CAAC,CAAC,CAAA;QAE7B,IAAI,SAAS,GAAG,SAAS,CAAA;QACzB,MAAM,SAAS,GAAa,EAAE,CAAA;QAE9B,KAAK,MAAM,IAAI,IAAI,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;YACtC,IAAI,IAAI,KAAK,EAAE,IAAI,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC;gBAAE,SAAQ,CAAC,gCAAgC;YAElF,MAAM,KAAK,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAA;YAC/B,MAAM,KAAK,GAAG,KAAK,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,CAAA;YACxD,IAAI,KAAK,GAAG,KAAK,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,GAAG,CAAC,CAAC,CAAA;YACrD,IAAI,KAAK,CAAC,UAAU,CAAC,GAAG,CAAC;gBAAE,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAA;YAEjD,IAAI,KAAK,KAAK,OAAO;gBAAE,SAAS,GAAG,KAAK,CAAA;iBACnC,IAAI,KAAK,KAAK,MAAM;gBAAE,SAAS,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;QACjD,CAAC;QAED,IAAI,SAAS,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC1B,MAAM,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;QAC9D,CAAC;IACF,CAAC;IAED,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,EAAE,CAAA;AAC7B,CAAC"}
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@extenshi/mcp",
3
+ "version": "0.0.1",
4
+ "description": "Extenshi MCP server — bring the extension catalog (search, security, market research) and pre-publish scanning into your AI tools",
5
+ "type": "module",
6
+ "publishConfig": {
7
+ "access": "public"
8
+ },
9
+ "bin": {
10
+ "extenshi-mcp": "dist/index.js"
11
+ },
12
+ "files": [
13
+ "dist"
14
+ ],
15
+ "scripts": {
16
+ "build": "tsc",
17
+ "dev": "tsc --watch",
18
+ "test": "vitest run",
19
+ "prepublishOnly": "yarn build"
20
+ },
21
+ "dependencies": {
22
+ "@trpc/client": "^11.17.0",
23
+ "fastmcp": "^4.0.1",
24
+ "undici": "^7.3.0",
25
+ "zod": "^3.25.76"
26
+ },
27
+ "devDependencies": {
28
+ "@types/node": "^22.16.5",
29
+ "typescript": "^5.8.3",
30
+ "vitest": "^3.2.4"
31
+ },
32
+ "engines": {
33
+ "node": ">=20"
34
+ }
35
+ }