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:
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.
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.
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-..." }, ...]