@extenshi/mcp
Version:
Extenshi MCP server — bring the extension catalog (search, security, market research) and pre-publish scanning into your AI tools
151 lines • 7.87 kB
TypeScript
/**
* 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;
/**
* The remote connector's counterpart of MISSING_KEY_MESSAGE. It signs in over
* OAuth, so a rejected request means the sign-in lapsed — there is no key to find.
*/
export declare const RECONNECT_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";
/**
* How the caller signs in. The stdio package reads an `ek_…` API key; the remote
* connector signs in over OAuth and has no key at all, so any text that sends the
* user to find one is wrong there.
*/
export type McpAuth = 'api-key' | 'oauth';
/**
* Shown to the model at `initialize`. It states what the tools return and what
* the account can do — not how to work — and is kept short: directory review
* reads this block in full, and a cut-off copy hides the end of it. The length
* is pinned by a test.
*/
export declare const SERVER_INSTRUCTIONS: string;
/** Match initialize instructions to the same capabilities used for registration. */
export declare function getServerInstructions(capabilities: ReadonlySet<Capability>, auth?: McpAuth): 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;
/**
* How this transport signs the caller in (default `api-key`, the stdio package).
* The remote connector passes `oauth`, which swaps every "get an API key" hint
* for the reconnect path.
*/
auth?: McpAuth;
/** Optional override of the missing-key message (defaults to the one for `auth`). */
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