> ## Documentation Index
> Fetch the complete documentation index at: https://docs.waniwani.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Knowledge Base

> Ingest markdown files and search with embeddings.

<Note>
  **Platform feature.** Requires `WANIWANI_API_KEY`. Works whether your MCP server is self-hosted or on Managed Hosting. [About the Platform](/sdk/platform/overview).
</Note>

The SDK includes a knowledge base client for semantic search. Ingest markdown files, then search from any tool handler.

## Setup

The KB client is available on the Waniwani client instance:

```ts theme={null}
import { waniwani } from "@waniwani/sdk";

const wani = waniwani(); // reads WANIWANI_API_KEY from env
```

## Ingest files

Load markdown content into your knowledge base. Ingestion is **destructive** and replaces all existing chunks.

```ts theme={null}
await wani.kb.ingest([
  {
    filename: "pricing.md",
    content: "# Pricing\n\nOur plans start at $9/month...",
    metadata: { category: "billing" },
  },
  {
    filename: "returns.md",
    content: "# Return Policy\n\n30-day return window...",
    metadata: { category: "support" },
  },
]);
```

The managed project template includes a ready-made `embed` script:

```bash theme={null}
bun embed
```

### Ingest script for self-hosted projects

If you're running your own MCP server, create a simple script to read your docs and push them to Waniwani. This runs standalone and doesn't need to be inside an MCP server.

```ts scripts/kb-ingest.ts theme={null}
import { readdir, readFile } from "node:fs/promises";
import { join } from "node:path";
import { waniwani } from "@waniwani/sdk";

const client = waniwani(); // reads WANIWANI_API_KEY from env

const knowledgeDir = join(import.meta.dirname, "../knowledge");
const files = await readdir(knowledgeDir);
const mdFiles = files.filter((f) => f.endsWith(".md"));

console.log(`Ingesting ${mdFiles.length} files from ${knowledgeDir}`);

const docs = await Promise.all(
  mdFiles.map(async (filename) => ({
    filename,
    content: await readFile(join(knowledgeDir, filename), "utf-8"),
  })),
);

const result = await client.kb.ingest(docs);
console.log(`Done: ${result.chunksIngested} chunks from ${result.filesProcessed} files`);
```

Run it with `npx tsx scripts/kb-ingest.ts` or `bun scripts/kb-ingest.ts`.

<Info>
  A built-in `waniwani embed` CLI command is coming soon to `@waniwani/cli`, so you won't need a custom script.
</Info>

### Ingest input

<ResponseField name="filename" type="string" required>
  Identifier for the source file.
</ResponseField>

<ResponseField name="content" type="string" required>
  Markdown content. Chunked server-side.
</ResponseField>

<ResponseField name="metadata" type="Record<string, string>">
  Arbitrary key-value pairs stored with each chunk. Can be used to filter search results.
</ResponseField>

### Ingest response

```ts theme={null}
{ chunksIngested: number; filesProcessed: number }
```

## Search

```ts theme={null}
const results = await wani.kb.search("What is the return policy?", {
  topK: 5,       // 1–20, default 5
  minScore: 0.3, // 0–1, default 0.3
  metadata: { category: "support" }, // exact-match filter
});
```

Each result contains:

```ts theme={null}
{
  source: string;    // filename
  heading: string;   // section heading
  content: string;   // chunk text
  score: number;     // similarity score (0–1)
  metadata?: Record<string, string>;
}
```

### Using search in a tool

```ts theme={null}
server.registerTool(
  "faq",
  {
    title: "FAQ",
    description: "Answer questions from the knowledge base",
    inputSchema: { question: z.string() },
    annotations: { readOnlyHint: true },
  },
  async ({ question }) => {
    const results = await wani.kb.search(question, { topK: 5 });

    if (results.length === 0) {
      return {
        content: [{ type: "text", text: "No relevant information found." }],
      };
    }

    const formatted = results
      .map((r) => `### ${r.heading}\n${r.content}`)
      .join("\n\n");

    return {
      content: [{ type: "text", text: formatted }],
    };
  },
);
```

## List sources

List all ingested files with their chunk counts:

```ts theme={null}
const sources = await wani.kb.sources();
// [{ source: "pricing.md", chunkCount: 12, createdAt: "2025-..." }, ...]
```
