/** * Helpers for injecting bundle skill `instructions` into the MCP `initialize` * response. * * Design notes: * - The `initialize` response's `instructions` field is server-side prompt * text that clients usually inject verbatim into the model context. Pushing * every skill's full SKILL.md body would balloon every initialize response * and burn token budget on tool calls. Instead, we ship a bounded catalog * summary (`**name**: description`) plus a pointer to the `skills://catalog` * resource and the `skills://{name}/SKILL.md` resource template for the full * content. * - Resolution is sync — the registry already holds metadata at boot. The * per-skill `instructions` markdown body stays lazy and reachable through * `skills://` resources or the `skills/search` and `skills/load` MCP * extension methods. * - Off / replace policies short-circuit before we touch the registry, so a * server with `injectInstructions: 'off'` pays zero cost regardless of how * many bundle skills are loaded. * - The catalog is recomputed lazily on every `initialize` request (see * `initialize-request.handler.ts`), so dynamic skill registrations made * after the server boots are reflected in subsequent reconnects without a * restart. */ import type { SkillRegistryInterface } from './skill.registry'; export type InjectInstructionsPolicy = 'off' | 'append' | 'prepend' | 'replace'; interface ComposeOptions { /** User-provided server instructions from `@FrontMcp({ instructions })`. */ userInstructions?: string; /** Framework-emitted hints (e.g. channel reply-tool guidance). */ channelInstructions?: string; /** Skill registry; pass `undefined` if skills are disabled. */ skillRegistry?: SkillRegistryInterface; /** Merge policy from `skillsConfig.injectInstructions`. */ policy?: InjectInstructionsPolicy; } /** * Sanitize a skill description so it can be embedded as a single bullet in a * markdown catalog without breaking out of its line context. * * - Collapses whitespace (newlines, tabs, multiple spaces) to single spaces. * - Escapes backslashes FIRST so a hostile `\*` in the input can't smuggle a * literal `\` into the output that pairs with our own escape and re-enables * the metacharacter (CodeQL: js/incomplete-sanitization). * - Escapes backticks so embedded code spans don't swallow the closing fence. * - Strips standalone `---` lines (would collide with section separators or * render as a horizontal rule). * - Escapes `*` and `_` so embedded `**bold**` / `_em_` / list markers don't * reflow our `**name**: description` formatting. * - Escapes `[` so embedded `[link](url)` syntax stays inert. * * Names themselves are NOT escaped — they pass kebab-case validation * (≤64 chars, `[a-z0-9-]+`) and never contain markdown specials. If the name * regex is ever relaxed, this helper must escape names too. * * Exported so the security test suite can pin down the escape ordering. */ export declare function sanitizeDescription(raw: string | undefined): string; /** * Sanitize a skill NAME for safe embedding as a bold catalog label. The name is * attacker-influenced (bundle-supplied) and was previously interpolated RAW into * `- **${name}**:` — a name with NEWLINES could break out of its bullet and * inject fake catalog lines / "SYSTEM:" framing into the catalog (surfaced in the * `search_skill` tool description AND the initialize `instructions`). Strip all * line breaks + control chars (the STRUCTURAL injection vector), collapse * whitespace, and hard-cap the length. Emphasis chars (`_`/`*`) are intentionally * NOT escaped — they're common in legitimate identifiers and a single line of * markdown can't inject cross-line instructions; malicious TEXT content (vs * structure) is defended by bundle signing, not by escaping. */ export declare function sanitizeName(raw: string | undefined): string; /** * Build a bounded catalog summary of MCP-visible skills. * * Output shape: * * ``` * Available skills (read the `skills://catalog` resource to browse, or `skills://{name}/SKILL.md` for full content): * * - **skill_name**: short description * - **another_skill**: ... * ``` * * Returns `''` if the registry is missing or has no MCP-visible skills. * The total length is hard-capped at `MAX_SKILL_CATALOG_CHARS` (footer * reserve included), with a `_(catalog truncated — showing N of M skills…)_` * footer appended when the cap is hit. */ export declare function buildSkillsCatalogSummary(skillRegistry: SkillRegistryInterface | undefined): string; /** * Compose the final `instructions` string for the MCP `initialize` response * by merging server instructions, channel hints, and the skill catalog * summary per the configured policy. * * Sections are joined with `\n\n---\n\n` so clients can render them as * separate logical blocks if they wish. * * Policy semantics: * - `'off'`: skips the skill catalog; user + channel hints are still emitted. * - `'append'` (default): user + channel + catalog, in that order. * - `'prepend'`: catalog + channel + user. * - `'replace'`: only the user-supplied instructions are emitted. **Falls * back to `'append'` semantics when `userInstructions` is empty/undefined** * so a misconfigured server doesn't silently drop the catalog and channel * hints. Document this in the schema if you change it. */ export declare function composeInitializeInstructions(options: ComposeOptions): string; /** * Build the channel-instructions hint emitted to clients when the scope * exposes any channel. Shared by stdio (`front-mcp.ts`) and HTTP * (`transport.local.adapter.ts`) so both transports stay in sync. * * Returns `''` when no channels are registered. */ export declare function buildChannelInstructions(channels: ChannelRegistryLike | undefined): string; /** * Minimal duck-typed surface for channel-registry consumers used by * `buildChannelInstructions`. Keeps this helper free of a hard dependency on * the channel module so it remains importable from the public barrel. */ interface ChannelRegistryLike { hasAny(): boolean; getChannelInstances(): Array<{ twoWay?: boolean; }>; } export {}; //# sourceMappingURL=skill-instructions.helper.d.ts.map