import { type Token } from '@frontmcp/di'; import { type ServerCapabilities } from '@frontmcp/protocol'; import { type EntryLineage, type EntryOwnerRef, type ScopeEntry, type SkillEntry, type SkillToolValidationMode, type SkillType } from '../common'; import { type SkillContent } from '../common/interfaces'; import { type SkillRecord } from '../common/records'; import type ProviderRegistry from '../provider/provider.registry'; import { RegistryAbstract, type RegistryBuildMapResult } from '../regsitry'; import { type SkillValidationReport } from './errors/skill-validation.error'; import type { ExternalSkillProviderBase } from './providers/external-skill.provider'; import { type SkillIndexCache, type SkillIndexScoring } from './skill-index-cache.interface'; import { type SkillIndexEntry } from './sep-2640'; import { type SkillListOptions, type SkillListResult, type SkillLoadResult, type SkillSearchOptions, type SkillSearchResult } from './skill-storage.interface'; import { type SkillChangeEvent } from './skill.events'; import { type SkillInstance } from './skill.instance'; import type { SyncResult } from './sync/sync-state.interface'; /** * Indexed skill for efficient lookup. */ export interface IndexedSkill { token: Token; instance: SkillInstance; baseName: string; lineage: EntryLineage; ownerKey: string; qualifiedName: string; qualifiedId: string; source: SkillRegistry; } /** * Options for configuring SkillRegistry behavior. */ export interface SkillRegistryOptions { /** * Default validation mode for all skills in this registry. * Can be overridden per-skill via SkillMetadata.toolValidation. * * @default 'warn' */ defaultToolValidation?: SkillToolValidationMode; /** * Whether to fail the entire registry initialization if any skill fails validation. * Only applies when toolValidation is 'strict'. * * @default false */ failOnInvalidSkills?: boolean; /** * Ranking function for the in-memory search index (forwarded to the storage * provider's vector DB). `bm25` for stronger keyword relevance; defaults to * `cosine`. */ scoring?: SkillIndexScoring; /** * Optional snapshot cache for the search index so a cold start can restore it * instead of recomputing. Typically supplied later via {@link SkillRegistry.setIndexCache} * when the cache binding only exists at request time (e.g. Cloudflare KV). */ indexCache?: SkillIndexCache; } /** * Options for getting skills from the registry. */ export interface GetSkillsOptions { /** * Whether to include hidden skills. * @default false */ includeHidden?: boolean; /** * Filter by visibility context. * - 'mcp': Only skills visible via MCP (visibility = 'mcp' or 'both') * - 'http': Only skills visible via HTTP (visibility = 'http' or 'both') * - 'all': All skills regardless of visibility (default) */ visibility?: 'mcp' | 'http' | 'all'; } /** * Interface for SkillRegistry consumers. */ export interface SkillRegistryInterface { owner: EntryOwnerRef; /** * Get all skills in the registry. * @param options - Options for filtering skills (or boolean for backwards compatibility) */ getSkills(options?: boolean | GetSkillsOptions): SkillEntry[]; /** * Get the "executable" subset — skills that declare at least one tool OR * at least one referenced openapi operation. Drives `codecall:searchSkills`. */ getExecutableSkills(options?: GetSkillsOptions): SkillEntry[]; /** * Get the "knowledge-only" subset — skills with no tools and no referenced * openapi operations. Drives `codecall:searchKnowledge`. */ getKnowledgeOnlySkills(options?: GetSkillsOptions): SkillEntry[]; /** * Find a skill by name. * @param name - The skill name */ findByName(name: string): SkillEntry | undefined; /** * Find a skill by qualified name (includes owner prefix). * @param qualifiedName - The qualified name (e.g., "my-app:review-pr") */ findByQualifiedName(qualifiedName: string): SkillEntry | undefined; /** * Search for skills matching a query. * @param query - Search query string * @param options - Search options */ search(query: string, options?: SkillSearchOptions): Promise; /** * Load a skill by ID. * @param skillId - The skill identifier */ loadSkill(skillId: string): Promise; /** * List skills with pagination. * @param options - List options */ listSkills(options?: SkillListOptions): Promise; /** * Check if any skills exist. */ hasAny(): boolean; /** * Get total skill count. * @param options - Count options */ count(options?: { tags?: string[]; includeHidden?: boolean; }): Promise; /** * Subscribe to skill change events. * @param opts - Subscription options * @param cb - Callback function */ subscribe(opts: { immediate?: boolean; filter?: (skill: SkillEntry) => boolean; }, cb: (event: SkillChangeEvent) => void): () => void; /** * Get MCP capabilities for skills. */ getCapabilities(): Partial; /** * SEP-2640 §Discovery — extra resource-template entries for the * `skill://index.json` document. */ getSep2640IndexTemplates?(): SkillIndexEntry[]; /** * SEP-2640 ADR — extra archive entries for the `skill://index.json` * document. */ getSep2640IndexArchives?(): SkillIndexEntry[]; /** * SEP-2640 §Discovery — opt-in extra `skill://` URIs to surface in the * server `instructions` field. */ getSep2640InstructionUris?(): string[]; /** * Validate all skills against the current tool registry. * Should be called after all tools (including from plugins/adapters) are registered. * * @returns Validation report for all skills * @throws SkillValidationError if failOnInvalidSkills is true and any skill fails */ validateAllTools(): Promise; /** * Register a skill at runtime from a fully-resolved SkillContent. * * Used by plugins (e.g. plugin-skilled-openapi) that ingest skill bundles after * the registry has booted. Returns a handle whose `unregister()` removes the * skill again and fires another change event. Re-registering the same `id` * replaces the previous content. * * Dynamically-registered skills appear in {@link search}, {@link loadSkill}, * {@link listSkills}, {@link count}, and {@link getSkills}. They participate * in `notifications/skills/list_changed` broadcasts. * * @param content - The skill content to register * @param opts - Optional registration metadata (e.g. source identifier for diagnostics) * @returns Handle exposing `unregister()` and the resolved skill id */ registerSkillContent(content: SkillContent, opts?: { source?: string; }): Promise<{ id: string; unregister: () => Promise; }>; /** * Remove a previously-registered dynamic skill by id. * No-op (returns false) if the id was not registered dynamically. */ unregisterSkill(id: string): Promise; /** * Sync local skills to external storage. * Only available when an external provider in persistent mode is set. * * @returns Sync result with added/updated/unchanged/removed counts, or null if no external provider */ syncToExternal(): Promise; /** * Get the external provider if one is configured. */ getExternalProvider(): ExternalSkillProviderBase | undefined; /** * Check if the registry has an external provider. */ hasExternalProvider(): boolean; } /** * Registry for managing skills. * * The SkillRegistry is the main facade for: * - Managing local skills (registered via @Skill decorator or skill() helper) * - Searching skills using TF-IDF * - Loading skill content (including fetching from URLs) * - Tracking skill changes */ export default class SkillRegistry extends RegistryAbstract implements SkillRegistryInterface { /** Owner of this registry */ owner: EntryOwnerRef; /** Local skills indexed for lookup */ private localRows; /** Skills registered dynamically at runtime via {@link registerSkillContent} */ private dynamicRows; /** * Original SkillContent preserved verbatim for dynamic skills, so loadSkill * returns the exact content (including `actions[]` and `bundleVersion`) that * was registered, rather than a metadata-rebuild that loses extra fields. */ private dynamicContents; /** * Per-id generation counter for dynamic registrations. Bumped on every * registerSkillContent call so a stale unregister handle held by an earlier * caller cannot remove a replacement registration. */ private dynamicGenerations; /** Adopted skills from child registries */ private adopted; /** Children registries */ private children; /** O(1) indexes */ private byQualifiedId; private byName; private byOwnerAndName; /** Version and emitter for change tracking */ private version; private emitter; /** Internal storage provider for search */ private storageProvider; /** External skill provider for sync operations */ private externalProvider?; /** Tool validator for checking tool availability */ private toolValidator?; /** The scope this registry operates in */ readonly scope: ScopeEntry; /** Registry-level options for validation behavior */ private readonly options; /** SEP-2640 §Discovery — additional `mcp-resource-template` index entries. */ private sep2640IndexTemplates; /** SEP-2640 ADR — additional `archive` index entries. */ private sep2640IndexArchives; /** SEP-2640 §Discovery — extra `skill://` URIs to surface in server `instructions`. */ private sep2640InstructionUris; constructor(providers: ProviderRegistry, list: SkillType[], owner: EntryOwnerRef, options?: SkillRegistryOptions); protected buildMap(list: SkillType[]): RegistryBuildMapResult; protected buildGraph(): void; protected initialize(): Promise; /** * Adopt skills from a child registry. */ adoptFromChild(child: SkillRegistry, _childOwner: EntryOwnerRef): Promise; /** * Get all skills in the registry. * @param options - Options for filtering skills (or boolean for backwards compatibility) */ getSkills(options?: boolean | GetSkillsOptions): SkillEntry[]; /** * Get inline (local) skills only. */ getInlineSkills(): SkillInstance[]; /** * Get skills marked `alwaysLoad: true` in their metadata. The codecall * runtime merges these into every execute() invocation regardless of which * skills the agent passed, providing a per-server "standard library." * * Honours the same visibility and environment-availability gating as * {@link getSkills}; hidden skills CAN be always-loaded (a server may want * common helpers loaded without surfacing them in search). */ getAlwaysLoadedSkills(): SkillEntry[]; /** * Get the "executable" subset — skills that declare at least one tool OR * at least one referenced openapi operation. These are the skills * `codecall:searchSkills` ranks. Honours hidden-filtering + availability * gating like {@link getSkills}. */ getExecutableSkills(opts?: GetSkillsOptions): SkillEntry[]; /** * Get the "knowledge-only" subset — skills with no tools and no * referenced openapi operations. These are the skills * `codecall:searchKnowledge` ranks. Honours hidden-filtering + availability * gating like {@link getSkills}. */ getKnowledgeOnlySkills(opts?: GetSkillsOptions): SkillEntry[]; /** * Find a skill by name. */ findByName(name: string): SkillEntry | undefined; /** * Find a skill by qualified name. */ findByQualifiedName(qualifiedName: string): SkillEntry | undefined; /** * Search for skills matching a query. * * Always merges the dynamic-skill overlay on top of the storage provider's * results. For ids present in `dynamicContents`, the provider's score and * ranking are preserved, but the metadata is replaced with the dynamic * projection — otherwise a stale provider row (from a re-registration whose * provider update lagged or failed) would mask the newer dynamic content. * Provider-only rows pass through unchanged; overlay-only rows are appended. */ /** * Attach (or replace) the snapshot cache used to persist/restore the search * index — for runtimes where the cache binding only exists at request time * (e.g. a Cloudflare KV namespace on `env`). No-op if the storage provider * doesn't support index caching. Pair with {@link warmIndex} to build/restore * the index off the first request's critical path. */ setIndexCache(cache: SkillIndexCache | undefined): void; /** * Eagerly build (or restore from the cache) the search index now. Called by a * host after registration so the one-time index build (or a KV restore) is * paid here rather than inside the first `search`. No-op if unsupported. */ warmIndex(): Promise; search(query: string, options?: SkillSearchOptions): Promise; /** * Load a skill by ID or name. * Supports looking up by: * - metadata.id (the unique identifier) * - metadata.name (the display name) * - qualified name (owner:name format) */ loadSkill(skillId: string): Promise; /** * Build a SkillLoadResult for a dynamic SkillContent, running tool validation * and warning formatting consistently with the SkillInstance.load() path. * * Returns a deep-cloned SkillContent so callers cannot mutate the registry's * stored copy. The matching clone-on-write happens at registration time in * `registerSkillContent`, keeping the registry authoritative. */ private buildDynamicLoadResult; /** * List skills with pagination. * * For ids present in `dynamicContents`, the provider's slot is preserved but * its metadata is replaced with the dynamic projection so re-registrations * are visible even when the provider lagged. Overlay-only ids are appended * to the page; pagination is recomputed against the merged set so `total` * and `hasMore` stay correct. */ listSkills(options?: SkillListOptions): Promise; /** * Check if any skills exist. */ hasAny(): boolean; /** * Get total skill count. * * Sums the provider count with overlay rows the provider does not know * about so callers see the same surface as `search` / `listSkills`. */ count(options?: { tags?: string[]; includeHidden?: boolean; }): Promise; /** * Project a SkillContent into the SkillMetadata shape callers expect from * search / list. Used to surface dynamic skills regardless of whether the * storage provider was able to index them. Mirrors the metadata projection * inside `registerSkillContent` so search/list/load see the same shape. */ private dynamicSkillMetadata; private collectDynamicMetadata; /** * Lightweight scorer for the dynamic overlay. The full TF-IDF in the * memory provider is unnecessary here because dynamic content already * passes through the provider when mutable; this overlay only fires when * the provider is read-only or rejected the index write, in which case a * substring match against name + description is the conservative behavior. */ private searchDynamicOverlay; /** * Subscribe to skill change events. */ subscribe(opts: { immediate?: boolean; filter?: (skill: SkillEntry) => boolean; }, cb: (event: SkillChangeEvent) => void): () => void; /** * Get MCP capabilities for skills. * * Declares the SEP-2640 (Skills Extension) capability when any skills are * registered, so conformant clients know to look for `skill://` resources * and `skill://index.json`. The extension carries no settings today; an * empty object signals support. * * The SEP itself targets `capabilities.extensions[]` (per the * forthcoming SEP-2133 extensions surface). Until the upstream MCP * schema lands `extensions`, we ride the existing * `capabilities.experimental[]` slot, which the schema accepts as * `Record`. Both are emitted so clients on either side * of the schema cutover see the declaration. */ getCapabilities(): Partial; /** * SEP-2640 §Discovery: extra resource-template entries to include in * `skill://index.json` for parameterised skill namespaces. Default is * empty; hosts populate this via {@link addSep2640IndexTemplate}. */ getSep2640IndexTemplates(): SkillIndexEntry[]; /** * Register an `mcp-resource-template` index entry. */ addSep2640IndexTemplate(entry: SkillIndexEntry): void; /** * SEP-2640 ADR 2026-04-19: optional `type: "archive"` entries in the * index. Hosts that pack skills into ZIP/TAR resources for atomic delivery * register them here so `skill://index.json` advertises them. */ getSep2640IndexArchives(): SkillIndexEntry[]; addSep2640IndexArchive(entry: SkillIndexEntry): void; /** * SEP-2640 §Discovery — opt-in pointer to skill URIs from the server's * `instructions` field. When non-empty, the transport adapter prepends a * "Available skills:" block listing the URIs. */ getSep2640InstructionUris(): string[]; addSep2640InstructionUri(uri: string): void; /** * Validate all skills against the current tool registry. * Should be called after all tools (including from plugins/adapters) are registered. * * This method: * 1. Checks each skill's tool references against the tool registry * 2. Respects per-skill and registry-level validation modes * 3. Emits a 'validated' event with results * 4. Optionally throws if failOnInvalidSkills is enabled * * @returns Validation report for all skills * @throws SkillValidationError if failOnInvalidSkills is true and any skill fails */ validateAllTools(): Promise; /** * Register a skill at runtime from a fully-resolved SkillContent. * See {@link SkillRegistryInterface.registerSkillContent} for full contract. */ registerSkillContent(content: SkillContent, opts?: { source?: string; }): Promise<{ id: string; unregister: () => Promise; }>; /** * Remove a previously-registered dynamic skill by id. */ unregisterSkill(id: string): Promise; private asMutableProvider; private listAllIndexed; private listAllInstances; private reindex; private makeRow; private relineage; private bump; /** * Set an external storage provider for skills. * * In read-only mode: * - Search/list/load operations will query the external provider * * In persistent mode: * - Local skills will be synced to external storage on syncToExternal() * - Search/list/load operations still use local storage * * @param provider - External provider instance extending ExternalSkillProviderBase */ setExternalProvider(provider: ExternalSkillProviderBase): void; /** * Get the external provider if one is configured. */ getExternalProvider(): ExternalSkillProviderBase | undefined; /** * Check if the registry has an external provider. */ hasExternalProvider(): boolean; /** * Sync local skills to external storage. * * This method: * 1. Collects all local skill content * 2. Calls syncSkills on the external provider * 3. Returns the sync result with changes detected * * Only available when an external provider in persistent mode is configured. * * @returns Sync result or null if no external provider or in read-only mode * * @example * ```typescript * const result = await registry.syncToExternal(); * if (result) { * console.log(`Synced: ${result.added.length} added, ${result.updated.length} updated`); * } * ``` */ syncToExternal(): Promise; } //# sourceMappingURL=skill.registry.d.ts.map