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:
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:
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.
Identifier for the source file.
Markdown content. Chunked server-side.
Arbitrary key-value pairs stored with each chunk. Can be used to filter search results.
Ingest response
{ chunksIngested: number; filesProcessed: number }
Search
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>;
}
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-..." }, ...]