Skip to main content
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:
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 — it replaces all existing chunks.
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:
bun embed

Ingest script for external projects

If you’re running your own MCP server, create a simple script to read your docs and push them to WaniWani:
scripts/embed.ts
import { waniwani } from "@waniwani/sdk";
import fs from "node:fs";
import path from "node:path";

const wani = waniwani(); // reads WANIWANI_API_KEY from env
const docsDir = "./knowledge-base"; // your markdown files

const files = fs.readdirSync(docsDir)
  .filter((f) => f.endsWith(".md"))
  .map((f) => ({
    filename: f,
    content: fs.readFileSync(path.join(docsDir, f), "utf-8"),
  }));

// Batch in groups of 10
for (let i = 0; i < files.length; i += 10) {
  const batch = files.slice(i, i + 10);
  const result = await wani.kb.ingest(batch);
  console.log(`Ingested ${result.filesProcessed} files (${result.chunksIngested} chunks)`);
}
Run it with npx tsx scripts/embed.ts or bun scripts/embed.ts.
A built-in waniwani embed CLI command is coming soon to @waniwani/cli, so you won’t need a custom script.

Ingest input

filename
string
required
Identifier for the source file.
content
string
required
Markdown content. Chunked server-side.
metadata
Record<string, string>
Arbitrary key-value pairs stored with each chunk. Can be used to filter search results.

Ingest response

{ chunksIngested: number; filesProcessed: number }
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:
{
  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

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:
const sources = await wani.kb.sources();
// [{ source: "pricing.md", chunkCount: 12, createdAt: "2025-..." }, ...]