Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/little-banks-bathe.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@speakeasy-api/docs-mcp-server": minor
"@speakeasy-api/docs-mcp-core": minor
"@speakeasy-api/docs-mcp-cli": minor
---

Added MCP prompt template support to docs-mcp: docs authors can now define prompts with \*.template.md (simple single-message prompts) or \*.template.yaml (structured multi-message prompts), with mustache argument rendering at runtime. Prompt templates are excluded from search indexing, surfaced through MCP prompts/list and prompts/get, and when both formats exist for the same prompt name, YAML is preferred with a warning.
14 changes: 13 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ A lightweight, domain-agnostic hybrid search engine for markdown (`.md`) corpora
- **Faceted taxonomy** — metadata keys become enum-injected JSON Schema filters on the search tool
- **Vector collapse** — deduplicates near-identical cross-language results at search time
- **Incremental builds** — embedding cache fingerprints each chunk; only changed content is re-embedded
- **MCP prompt templates** — register reusable prompts from docs using `*.template.md` (single-message shorthand) or `*.template.yaml` (multi-message format) with mustache argument rendering
- **Graceful degradation**
- _Chunking_ — chunk sizes adapt to the configured embedding provider's context window; falls back to conservative defaults when no provider is set
- _Query_ — if the embedding API errors at runtime (downtime, expired credits, network issues), the server falls back to FTS-only search with a one-time warning
Expand Down Expand Up @@ -99,7 +100,7 @@ We recommend starting with FTS-only search. While embeddings improve relevance f

## Supported File Types

The indexer processes **`.md` (Markdown)** files. Files are discovered via the `**/*.md` glob pattern within the configured docs directory. YAML frontmatter is supported for per-file metadata and chunking overrides.
The indexer processes **`.md` (Markdown)** files. Files are discovered via the `**/*.md` glob pattern within the configured docs directory. YAML frontmatter is supported for per-file metadata and chunking overrides. Prompt templates are also discovered from `*.template.md` and `*.template.yaml`, and are excluded from search indexing.

## Corpus Structure

Expand Down Expand Up @@ -218,6 +219,17 @@ The tools exposed to the agent are dynamically generated based on your `corpus_d
| `search_docs` | Performs hybrid search. Tool names and descriptions are user-configurable. Parameters are dynamically generated with valid taxonomy injected as JSON Schema `enum`s. Supports stateless cursor pagination. Returns fallback hints on zero results. |
| `get_doc` | Returns a specific chunk, plus `context: N` neighboring chunks for surrounding detail. |

## MCP Prompts

Prompt definitions are discovered at build time and exposed over MCP via `prompts/list` and `prompts/get`.

See [docs/prompt-templates.md](docs/prompt-templates.md) for full usage guidelines and examples.

- `*.template.md` — Markdown body shorthand for a single `user` text message
- `*.template.yaml` — Structured prompt format for multiple messages/content parts
- Mustache templating is applied to prompt text content at runtime
- If both formats exist for the same prompt name, YAML is preferred and a warning is emitted during `build` and `validate`

## Quick Start

### FTS-Only (Recommended)
Expand Down
24 changes: 24 additions & 0 deletions docs/implementation/mcp_tool_contracts.md
Original file line number Diff line number Diff line change
Expand Up @@ -174,3 +174,27 @@ The `get_doc` success response uses a stable delimiter format for chunk boundari
- `Chunk {N} of {M}`: 1-indexed position within the source file.
- `Target`: The requested chunk. Exactly one delimiter per response carries this label.
- `Context: {+N|-N}`: A context chunk. `+1` means one chunk after the target, `-1` means one before.

---

## MCP Prompts

In addition to tools, the server exposes MCP prompts discovered from `*.template.md` and `*.template.yaml` files at build time.

### `prompts/list`

- Returns prompt definitions from `metadata.json.prompts`.
- Each prompt includes `name`, optional `title`, optional `description`, and optional `arguments`.
- Prompt argument fields exposed over MCP are `name`, optional `description`, and optional `required`.

### `prompts/get`

- Input:
- `name` (required): prompt name.
- `arguments` (optional): string-to-string map used for mustache templating.
- Behavior:
- Resolves prompt by `name`.
- Validates all `required: true` arguments are present and non-empty.
- Renders all prompt text content with mustache.
- Output:
- `GetPromptResult` with one or more rendered messages.
34 changes: 33 additions & 1 deletion docs/implementation/metadata_contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,33 @@ This document defines the schema for `metadata.json`, the build artifact generat

// The embedding provider used at build time (if any).
// Null means FTS-only index — the server disables vector search.
"embedding": null
"embedding": null,

// Optional MCP prompts discovered from *.template.md and *.template.yaml files.
// These files are excluded from search indexing and exposed via prompts/list + prompts/get.
"prompts": [
{
"name": "guides/convert-currency",
"title": "Convert Currency",
"description": "Convert 100 USD to a target currency.",
"arguments": [
{
"name": "currency",
"description": "The target currency for conversion",
"required": true
}
],
"messages": [
{
"role": "user",
"content": {
"type": "text",
"text": "Convert 100 USD to {{currency}}. Use current forex mcp tools and APIs if available."
}
}
]
}
]
}
```

Expand Down Expand Up @@ -79,6 +105,11 @@ This document defines the schema for `metadata.json`, the build artifact generat
- **`embedding`**
- Type: `null | { provider: string; model: string; dimensions: number }`
- If object is provided, `dimensions` must be `> 0`.
- **`prompts`**
- Type: optional array of `{ name, title?, description?, arguments[], messages[] }`.
- `name` is required non-empty string.
- `messages` must be a non-empty array of `{ role, content }` where `role` is `user|assistant` and `content` is currently text (`{ type: "text", text }`).
- `arguments` entries require `name`; support optional `description` and `required`.

## Rationale for each field

Expand All @@ -87,6 +118,7 @@ This document defines the schema for `metadata.json`, the build artifact generat
- **`taxonomy`**: This is the load-bearing field for the Dynamic Schema feature. The server reads it at boot to inject `enum` arrays into the `search_docs` JSON Schema. Keys are strictly dynamic (not hardcoded to `language`), guaranteeing the core engine remains domain-agnostic per the architecture's design goal.
- **`stats`**: Cheap to compute at index time, useful for the eval harness and the Host's telemetry pipeline. `source_commit` adds lightweight source provenance without introducing brittle boot-time coupling.
- **`embedding`**: The runtime server needs to know whether to execute vector search pathways (`table.search().nearestTo()`) or fall back to pure FTS if the index was built with `--embedding-provider none`. It is also required by the eval harness to record the specific provider/model permutation in its markdown delta reports.
- **`prompts`**: Allows docs authors to ship reusable MCP prompts next to docs content via `*.template.md` (single user-text shorthand) and `*.template.yaml` (multi-message format), while keeping prompt templates out of search indexing.

## System Boundaries (Who Writes vs. Reads)

Expand Down
101 changes: 101 additions & 0 deletions docs/prompt-templates.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# Prompt Templates

Docs MCP supports prompt templates that are discovered during `docs-mcp build` and exposed through MCP `prompts/list` and `prompts/get`.

## Why Use Prompt Templates

Prompt templates let you package reusable prompt workflows alongside your docs corpus so agents can discover and invoke them consistently.

## File Naming and Discovery

- Place prompt templates anywhere under your docs directory.
- Supported file types:
- `*.template.md` (single-message shorthand)
- `*.template.yaml` (structured, multi-message format)
- Prompt templates are excluded from search indexing (`search_docs` / `get_doc` corpus chunks).

Prompt names are derived from the relative file path without suffix:

- `guides/auth-integration.template.md` -> `guides/auth-integration`
- `guides/webhook-debug-playbook.template.yaml` -> `guides/webhook-debug-playbook`

## Conflict Rule (Markdown vs YAML)

If both `foo.template.md` and `foo.template.yaml` exist for the same derived prompt name:

- Docs MCP emits a warning during both `build` and `validate`
- YAML is preferred
- Markdown variant is ignored for that prompt name

## Mustache Templating

- Text content uses mustache placeholders (for example `{{auth_method}}`).
- At runtime, `prompts/get` renders placeholders using provided `arguments`.
- Required arguments are validated before rendering.

## Format: `*.template.md` (Shorthand)

Use this for simple prompts that return one `user` text message.

```md
---
title: AcmeAuth Integration Advisor
description: Generate implementation guidance using authentication, rate limiting, and webhook docs.
arguments:
- name: app_type
description: The kind of app being integrated
required: true
- name: auth_method
description: Preferred auth method
required: true
---

You are helping integrate AcmeAuth for a {{app_type}}.
Use {{auth_method}} as the primary authentication method.
Include authentication, rate-limiting, and webhook setup guidance.
```

## Format: `*.template.yaml` (Structured)

Use this when you need multiple messages or more explicit structure.

```yaml
title: Webhook Delivery Debug Playbook
description: Triage and resolve failed webhook deliveries.
arguments:
- name: event_type
required: true
- name: status_code
required: true
- name: retry_attempt
messages:
- role: user
content:
type: text
text: |
Investigate a failing webhook delivery for event {{event_type}}.
The endpoint returned HTTP {{status_code}} on retry {{retry_attempt}}.
- role: user
content:
type: text
text: |
Provide likely root causes, remediation steps,
signature verification checks, and a retry checklist.
```

## Argument Schema

Arguments are validated with a simple schema:

- `name` (required)
- `title` (optional)
- `description` (optional)
- `required` (optional, boolean)

## Authoring Guidelines

- Prefer `.template.md` for concise single-workflow prompts.
- Use `.template.yaml` for multi-step or multi-message flows.
- Keep argument names stable and descriptive.
- Mark only truly required arguments as `required: true`.
- Write prompt text so it can stand alone without hidden assumptions.
76 changes: 76 additions & 0 deletions packages/cli/src/discovery.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import path from "node:path";
import fg from "fast-glob";

export async function listMarkdownFiles(docsDir: string): Promise<string[]> {
const files = await fg(["**/*.md"], {
cwd: docsDir,
absolute: true,
onlyFiles: true,
dot: false,
ignore: ["**/*.template.md", "**/*.template.yaml"],
});
files.sort((a, b) => a.localeCompare(b));
return files;
}

export async function listPromptFiles(docsDir: string): Promise<string[]> {
const files = await fg(["**/*.template.md", "**/*.template.yaml"], {
cwd: docsDir,
absolute: true,
onlyFiles: true,
dot: false,
});
files.sort((a, b) => a.localeCompare(b));
return files;
}

export function derivePromptName(filePath: string, docsDir: string): string {
const relativePath = toPosix(path.relative(docsDir, filePath));
return relativePath.replace(/\.template\.(md|yaml)$/u, "");
}

export type PromptTemplateFormat = "markdown" | "yaml";

export function getPromptTemplateFormat(filePath: string): PromptTemplateFormat {
if (filePath.endsWith(".template.yaml")) {
return "yaml";
}
return "markdown";
}

export function resolvePreferredPromptFiles(
files: string[],
docsDir: string,
onWarning: (message: string) => void,
): string[] {
const byName = new Map<string, string>();
for (const filePath of files) {
const name = derivePromptName(filePath, docsDir);
const incomingFormat = getPromptTemplateFormat(filePath);
const existing = byName.get(name);
if (!existing) {
byName.set(name, filePath);
continue;
}

const existingFormat = getPromptTemplateFormat(existing);
if (existingFormat === incomingFormat) {
throw new Error(
`Duplicate prompt template '${name}' with the same format at '${toPosix(path.relative(docsDir, existing))}' and '${toPosix(path.relative(docsDir, filePath))}'`,
);
}

const yamlPath = incomingFormat === "yaml" ? filePath : existing;
const markdownPath = incomingFormat === "markdown" ? filePath : existing;
onWarning(
`Found both markdown and yaml templates for '${name}' (${toPosix(path.relative(docsDir, markdownPath))} and ${toPosix(path.relative(docsDir, yamlPath))}); using yaml`,
);
byName.set(name, yamlPath);
}

return [...byName.values()].sort((a, b) => a.localeCompare(b));
}

function toPosix(input: string): string {
return input.split(path.sep).join("/");
}
Loading