Skip to main content
Platform feature. Requires WANIWANI_API_KEY. Works whether your MCP server is self-hosted or on Managed Hosting. About the Platform.
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 and 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 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.
scripts/kb-ingest.ts
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.
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-..." }, ...]