#!/usr/bin/env node
/**
* KB-MCP Server - Knowledge Base MCP Server
* A local-first knowledge management system for AI agents
*
* @author Matrix Agent
* @license MIT
*/
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import { randomUUID } from "crypto";
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
import { join } from "path";
// ============================================================================
// Types & Interfaces
// ============================================================================
interface Document {
id: string;
title: string;
content: string;
metadata: Record<string, unknown>;
embedding?: number[];
createdAt: string;
updatedAt: string;
}
interface KnowledgeStore {
documents: Document[];
version: string;
}
// ============================================================================
// Simple Embedding Generator (TF-IDF style for local use)
// ============================================================================
function generateEmbedding(text: string): number[] {
const words = text.toLowerCase().split(/\W+/).filter(w => w.length > 2);
const embedding = new Array(128).fill(0);
for (const word of words) {
let hash = 0;
for (let i = 0; i < word.length; i++) {
hash = ((hash << 5) - hash) + word.charCodeAt(i);
hash = hash & hash;
}
const index = Math.abs(hash) % 128;
embedding[index] += 1;
}
const magnitude = Math.sqrt(embedding.reduce((sum, val) => sum + val * val, 0));
return magnitude > 0 ? embedding.map(v => v / magnitude) : embedding;
}
function cosineSimilarity(a: number[], b: number[]): number {
if (a.length !== b.length) return 0;
let dotProduct = 0;
for (let i = 0; i < a.length; i++) {
const aVal = a[i] ?? 0;
const bVal = b[i] ?? 0;
dotProduct += aVal * bVal;
}
return dotProduct;
}
// ============================================================================
// Knowledge Store
// ============================================================================
const DATA_DIR = process.env.KB_DATA_DIR || join(process.cwd(), ".kb-data");
const STORE_FILE = join(DATA_DIR, "knowledge.json");
function ensureDataDir(): void {
if (!existsSync(DATA_DIR)) {
mkdirSync(DATA_DIR, { recursive: true });
}
}
function loadStore(): KnowledgeStore {
ensureDataDir();
if (existsSync(STORE_FILE)) {
try {
const data = readFileSync(STORE_FILE, "utf-8");
return JSON.parse(data);
} catch {
return { documents: [], version: "1.0.0" };
}
}
return { documents: [], version: "1.0.0" };
}
function saveStore(store: KnowledgeStore): void {
ensureDataDir();
writeFileSync(STORE_FILE, JSON.stringify(store, null, 2));
}
let store = loadStore();
// ============================================================================
// MCP Server Setup
// ============================================================================
const server = new McpServer({
name: "kb-mcp-server",
version: "1.0.0",
});
// ============================================================================
// Tool: ingest_document
// ============================================================================
server.tool(
"ingest_document",
"Ingest a document into the knowledge base. Stores content with embeddings for semantic search.",
{
title: z.string().describe("Document title"),
content: z.string().describe("Document content (text, markdown, etc.)"),
metadata: z.record(z.string(), z.unknown()).optional().describe("Optional metadata"),
},
async ({ title, content, metadata = {} }) => {
const now = new Date().toISOString();
const doc: Document = {
id: randomUUID(),
title,
content,
metadata,
embedding: generateEmbedding(title + " " + content),
createdAt: now,
updatedAt: now,
};
store.documents.push(doc);
saveStore(store);
return {
content: [{
type: "text",
text: JSON.stringify({ success: true, id: doc.id, title: doc.title }, null, 2),
}],
};
}
);
// ============================================================================
// Tool: query_knowledge
// ============================================================================
server.tool(
"query_knowledge",
"Query the knowledge base using semantic search.",
{
query: z.string().describe("Search query"),
limit: z.number().optional().default(5).describe("Max results"),
threshold: z.number().optional().default(0.1).describe("Min similarity"),
},
async ({ query, limit = 5, threshold = 0.1 }) => {
const queryEmbedding = generateEmbedding(query);
const results = store.documents
.map(doc => ({ ...doc, similarity: cosineSimilarity(queryEmbedding, doc.embedding || []) }))
.filter(doc => doc.similarity >= threshold)
.sort((a, b) => b.similarity - a.similarity)
.slice(0, limit)
.map(({ embedding, ...rest }) => rest);
return {
content: [{
type: "text",
text: JSON.stringify({ query, count: results.length, results }, null, 2),
}],
};
}
);
// ============================================================================
// Tool: list_documents
// ============================================================================
server.tool(
"list_documents",
"List all documents in the knowledge base.",
{
limit: z.number().optional().default(20).describe("Max documents"),
offset: z.number().optional().default(0).describe("Offset"),
},
async ({ limit = 20, offset = 0 }) => {
const docs = store.documents
.slice(offset, offset + limit)
.map(({ embedding, content, ...rest }) => ({
...rest,
contentPreview: content.substring(0, 200) + (content.length > 200 ? "..." : ""),
}));
return {
content: [{
type: "text",
text: JSON.stringify({ total: store.documents.length, count: docs.length, documents: docs }, null, 2),
}],
};
}
);
// ============================================================================
// Tool: get_document
// ============================================================================
server.tool(
"get_document",
"Get a specific document by ID.",
{ id: z.string().describe("Document ID") },
async ({ id }) => {
const doc = store.documents.find(d => d.id === id);
if (!doc) {
return { content: [{ type: "text", text: JSON.stringify({ error: "Not found" }) }] };
}
const { embedding, ...docWithoutEmbedding } = doc;
return { content: [{ type: "text", text: JSON.stringify(docWithoutEmbedding, null, 2) }] };
}
);
// ============================================================================
// Tool: delete_document
// ============================================================================
server.tool(
"delete_document",
"Delete a document from the knowledge base.",
{ id: z.string().describe("Document ID") },
async ({ id }) => {
const index = store.documents.findIndex(d => d.id === id);
if (index === -1) {
return { content: [{ type: "text", text: JSON.stringify({ error: "Not found" }) }] };
}
const deleted = store.documents.splice(index, 1)[0]!;
saveStore(store);
return {
content: [{
type: "text" as const,
text: JSON.stringify({ success: true, deleted: deleted.title }),
}],
};
}
);
// ============================================================================
// Tool: update_document
// ============================================================================
server.tool(
"update_document",
"Update an existing document.",
{
id: z.string().describe("Document ID"),
title: z.string().optional().describe("New title"),
content: z.string().optional().describe("New content"),
metadata: z.record(z.string(), z.unknown()).optional().describe("New metadata"),
},
async ({ id, title, content, metadata }) => {
const doc = store.documents.find(d => d.id === id);
if (!doc) {
return { content: [{ type: "text", text: JSON.stringify({ error: "Not found" }) }] };
}
if (title) doc.title = title;
if (content) doc.content = content;
if (metadata) doc.metadata = { ...doc.metadata, ...metadata };
doc.updatedAt = new Date().toISOString();
doc.embedding = generateEmbedding(doc.title + " " + doc.content);
saveStore(store);
return { content: [{ type: "text", text: JSON.stringify({ success: true, id: doc.id }) }] };
}
);
// ============================================================================
// Tool: kb_stats
// ============================================================================
server.tool(
"kb_stats",
"Get knowledge base statistics.",
{},
async () => {
const totalDocs = store.documents.length;
const totalChars = store.documents.reduce((sum, d) => sum + d.content.length, 0);
return {
content: [{
type: "text",
text: JSON.stringify({
version: store.version,
totalDocuments: totalDocs,
totalCharacters: totalChars,
dataDirectory: DATA_DIR,
}, null, 2),
}],
};
}
);
// ============================================================================
// Start Server
// ============================================================================
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("KB-MCP Server running on stdio transport");
}
main().catch(console.error);