UNPKG

@extenshi/mcp

Version:

Extenshi MCP server — bring the extension catalog (search, security, market research) and pre-publish scanning into your AI tools

128 lines • 6.87 kB
/** * Transport-agnostic tool registry for the Extenshi MCP server. * * The SAME tool set must run over two transports: * - stdio (`index.ts`) — local, single `ek_…` key from the environment. * - remote (`http.ts`, in the sibling `@extenshi/mcp-server`) — Streamable * HTTP behind OAuth; identity is per-request (an access token → userId). * * To keep ONE source of truth for what the tools do, every tool body here is * identity-agnostic: it resolves its catalog client / scan auth through the * injected `ToolDeps` instead of reaching for a module-global key. stdio passes * deps that ignore the call context and use the env key; the remote server * passes deps that read `context.session` (the validated OAuth identity). * * Capability gating: the remote connector deliberately exposes a SUBSET — the * catalog/project/policy/guidance tools. `scan_extension` and `publish_extension` need * the caller's LOCAL filesystem and LOCAL store credentials, which a hosted * server has no access to (and must never store) — so they are registered only * when the corresponding capability is present (stdio enables all four). * See internal-docs/plans/2026-06-25-claude-connector-directory.md §13 #1. * * capability → tools * ───────────────────────────────────────────────────────────── * 'read' → search_extensions, get_extension, get_reviews, * get_security, get_risk_by_store_ids, market_overview, * list_my_projects, get_project_state, get_project_scaffold, * list/get/publish/update privacy policy (hosted; Pro) * 'docs' → get_development_guide, search_docs, list_extension_templates, generate_icon_workflow, * generate_welcome_page_workflow, generate_landing_page, localize_workflow (free; no key) * 'scan' → scan_extension (local artifact; stdio only) * 'publish' → publish_extension (local creds; stdio only) * * stdout is the MCP protocol channel for stdio — nothing here may write to it. */ import { type FastMCP, type FastMCPSessionAuth } from 'fastmcp'; import type { Bff } from './bff.js'; export declare const KEY_PAGE = "https://dojo.extenshi.io/api-keys"; export declare const SIGNUP_PAGE = "https://auth.extenshi.io/signup"; export declare const BILLING_PAGE = "https://dojo.extenshi.io/billing"; export declare const HOSTED_PAGES_DOCS = "https://docs.extenshi.io/developers/project-sync#hosted-pages-from-the-api"; export declare const MISSING_KEY_MESSAGE: string; /** * Max extensions `get_risk_by_store_ids` accepts per call. * * CROSS-PACKAGE SYNC POINT — nothing can import across it: this is a published * npm package, the enforcing code is `METERED_BATCH_MAX_EXTENSIONS` in * `catalog/catalog-bff/src/lib/metered-batch-cap.ts`. The BFF is the authority * (it rejects an over-cap metered batch); this constant only keeps the tool * schema and description honest so an agent is told the limit up front instead * of discovering it as a round-tripped error. Pinned by `tools.test.ts`. */ export declare const MAX_BATCH_EXTENSIONS = 40; export declare const SERVER_NAME = "extenshi"; export declare const SERVER_INSTRUCTIONS: string; /** Match initialize instructions to the same capabilities used for registration. */ export declare function getServerInstructions(capabilities: ReadonlySet<Capability>): string; /** Which tool groups to register on this transport. */ export type Capability = 'read' | 'docs' | 'scan' | 'publish'; /** * Minimal structural view of a FastMCP tool-execute context. The real context * (passed by FastMCP to `execute`) carries more (`reportProgress`, `log`, …); * we only need `session` for per-request identity resolution. Extra props are * fine — this is a structural supertype. */ export interface ToolCallContext { session?: FastMCPSessionAuth; } export interface ToolDeps { /** Base URLs for the BFF (reads), scan backend, and docs site. */ cfg: { bffUrl: string; scanUrl: string; docsUrl: string; }; /** Tool groups to register. */ capabilities: ReadonlySet<Capability>; /** * Build a catalog BFF client for THIS call's identity. * stdio: ignores `ctx`, uses the env key (throws MISSING_KEY if absent). * remote: reads `ctx.session` (already OAuth-authenticated) and binds the * caller's userId. */ getBff: (ctx: ToolCallContext) => Bff; /** * Resolve the scan/publish API key for THIS call (paid local ops). Required * only when the 'scan' or 'publish' capability is enabled. stdio supplies * the env key (or throws MISSING_KEY); the remote connector never enables * these capabilities, so it may omit this. */ requireApiKey?: (ctx: ToolCallContext) => string; /** * Non-throwing key accessor for the publish-access preflight, which fails * OPEN when no key is present (it must not hard-require a key). Returns the * key or undefined. stdio supplies the (nullable) env key. */ getApiKey?: (ctx: ToolCallContext) => string | undefined; /** Optional override of the missing-key message (defaults to MISSING_KEY_MESSAGE). */ missingKeyMessage?: string; } /** * True when a thrown error is an expected, user-facing condition rather than a * bug worth an exception report. * * `UserError` alone can't answer this. It has two jobs in this file: messages we * AUTHORED for the caller ("No extension found …", the missing-key help), and a * last-resort wrapper the catch-alls put around anything that escaped (readError, * the docs/scan handlers). Treating every `UserError` as expected would let that * second group launder a BFF 500 or a plain TypeError into "expected" and drop it * from error tracking — so the two are told apart by `cause`: * * - authored (no `cause`) → expected by construction: we wrote the message. * - wrapping (has `cause`) → only as expected as what it wraps; classifyError * follows the chain, so a 429 gate stays expected and a 500 does not. * * Corollary for anything that wraps a caught error: build it with * userErrorFrom(), or it reads as authored and its origin never gets captured. * * `kind` defaults to classifying `err`; pass one to reuse a classification the * caller already made, so the reported `error_kind` and this decision can't * disagree. */ export declare function isExpectedError(err: unknown, kind?: string): boolean; /** * Register the Extenshi tools once on a FastMCP server. * Only the tools whose capability is present in `deps.capabilities` are added. */ export declare function registerTools(server: FastMCP, deps: ToolDeps): void; //# sourceMappingURL=tools.d.ts.map