#!/usr/bin/env node
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
Tool,
} from "@modelcontextprotocol/sdk/types.js";
// Type definitions
interface KnowledgeEntry {
id: string;
content: string;
category: string;
metadata: {
source: string;
date: string;
};
}
// Simulated knowledge base (we'll replace this with Vectorize later)
const knowledgeBase: KnowledgeEntry[] = [
{
id: "1",
content: "FPL Hub handles 500,000+ API calls daily with 99.9% uptime using Cloudflare Workers",
category: "product",
metadata: { source: "production-metrics", date: "2024" },
},
{
id: "2",
content: "Cloudflare Workers AI provides access to LLMs like Llama, Mistral, and embedding models at the edge",
category: "ai",
metadata: { source: "cloudflare-docs", date: "2024" },
},
{
id: "3",
content: "Vectorize supports vector dimensions up to 1536 and uses HNSW indexing for fast similarity search",
category: "vectorize",
metadata: { source: "technical-specs", date: "2024" },
},
{
id: "4",
content: "MCP (Model Context Protocol) enables LLMs to securely access external data sources and tools",
category: "mcp",
metadata: { source: "anthropic-docs", date: "2024" },
},
{
id: "5",
content: "TypeScript MCP SDK provides server and client implementations with full type safety",
category: "mcp",
metadata: { source: "mcp-sdk", date: "2024" },
},
{
id: "6",
content: "D1 database queries return results in under 10ms within the same region using bound statements",
category: "database",
metadata: { source: "performance-tests", date: "2024" },
},
{
id: "7",
content: "RAG systems typically use chunk sizes of 500-1000 tokens with 10-20% overlap for optimal retrieval",
category: "ai",
metadata: { source: "rag-best-practices", date: "2024" },
},
{
id: "8",
content: "Workers AI embedding model 'bge-small-en-v1.5' produces 384-dimensional vectors optimized for English text",
category: "ai",
metadata: { source: "workers-ai-docs", date: "2024" },
},
];
// Simple cache implementation
interface CacheEntry {
data: string;
timestamp: number;
}
const cache = new Map<string, CacheEntry>();
const CACHE_TTL = 60000; // 60 seconds
function getCacheKey(toolName: string, args: any): string {
return `${toolName}:${JSON.stringify(args)}`;
}
function getFromCache(key: string): string | null {
const entry = cache.get(key);
if (!entry) return null;
// Check if expired
if (Date.now() - entry.timestamp > CACHE_TTL) {
cache.delete(key);
return null;
}
return entry.data;
}
function setCache(key: string, data: string): void {
cache.set(key, {
data,
timestamp: Date.now(),
});
}
// Simple keyword search (we'll upgrade to vector search)
function searchKnowledge(query: string, limit: number = 5): KnowledgeEntry[] {
const lowerQuery = query.toLowerCase();
return knowledgeBase
.filter((item) =>
item.content.toLowerCase().includes(lowerQuery) ||
item.category.toLowerCase().includes(lowerQuery)
)
.slice(0, limit);
}
// Initialize MCP server
const server = new Server(
{
name: "knowledge-base-server",
version: "1.0.0",
},
{
capabilities: {
tools: {},
},
}
);
// Define available tools
server.setRequestHandler(ListToolsRequestSchema, async () => {
const tools: Tool[] = [
{
name: "search_knowledge",
description:
"Search the knowledge base for relevant information. Use this when you need to find specific facts or documentation.",
inputSchema: {
type: "object",
properties: {
query: {
type: "string",
description: "Search query or keywords",
},
limit: {
type: "number",
description: "Maximum number of results to return",
default: 5,
},
},
required: ["query"],
},
},
{
name: "list_categories",
description: "List all available knowledge base categories",
inputSchema: {
type: "object",
properties: {},
},
},
{
name: "get_by_category",
description: "Get all knowledge base entries for a specific category",
inputSchema: {
type: "object",
properties: {
category: {
type: "string",
description: "Category to filter by (e.g., 'ai', 'technology', 'product')",
},
},
required: ["category"],
},
},
{
name: "get_by_id",
description: "Get a specific knowledge base entry by its ID",
inputSchema: {
type: "object",
properties: {
id: {
type: "string",
description: "The ID of the entry to retrieve",
},
},
required: ["id"],
},
},
{
name: "advanced_search",
description: "Search with additional filters like category and result limit",
inputSchema: {
type: "object",
properties: {
query: {
type: "string",
description: "Search query",
},
category: {
type: "string",
description: "Optional: filter by category",
},
limit: {
type: "number",
description: "Maximum results to return",
default: 5,
},
},
required: ["query"],
},
},
];
return { tools };
});
// Handle tool execution
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
// Generate cache key
const cacheKey = getCacheKey(name, args);
// Check cache first
const cached = getFromCache(cacheKey);
if (cached) {
console.error(`[CACHE HIT] ${name}`, args);
return {
content: [
{
type: "text",
text: cached,
},
],
};
}
console.error(`[CACHE MISS] ${name}`, args);
try {
if (name === "search_knowledge") {
const query = args?.query as string;
const limit = (args?.limit as number) || 5;
if (!query) {
throw new Error("Query parameter is required");
}
const results = searchKnowledge(query, limit);
const responseText = JSON.stringify(
{
query,
resultsCount: results.length,
results: results.map((r) => ({
id: r.id,
content: r.content,
category: r.category,
})),
},
null,
2
);
// Cache the response
setCache(cacheKey, responseText);
return {
content: [
{
type: "text",
text: responseText,
},
],
};
}
if (name === "list_categories") {
const categories = [...new Set(knowledgeBase.map((item) => item.category))];
const responseText = JSON.stringify(
{
categories,
count: categories.length,
},
null,
2
);
// Cache the response
setCache(cacheKey, responseText);
return {
content: [
{
type: "text",
text: responseText,
},
],
};
}
if (name === "get_by_category") {
const category = args?.category as string;
if (!category) {
throw new Error("Category parameter is required");
}
const results = knowledgeBase.filter(
(item) => item.category.toLowerCase() === category.toLowerCase()
);
if (results.length === 0) {
const responseText = JSON.stringify({
category,
message: "No entries found for this category",
availableCategories: [
...new Set(knowledgeBase.map((item) => item.category)),
],
});
// Don't cache errors
return {
content: [
{
type: "text",
text: responseText,
},
],
};
}
const responseText = JSON.stringify(
{
category,
count: results.length,
entries: results.map((r) => ({
id: r.id,
content: r.content,
metadata: r.metadata,
})),
},
null,
2
);
// Cache the response
setCache(cacheKey, responseText);
return {
content: [
{
type: "text",
text: responseText,
},
],
};
}
if (name === "get_by_id") {
const id = args?.id as string;
if (!id) {
throw new Error("ID parameter is required");
}
const entry = knowledgeBase.find((item) => item.id === id);
if (!entry) {
const responseText = JSON.stringify({
id,
error: "Entry not found",
availableIds: knowledgeBase.map((item) => item.id),
});
// Don't cache errors
return {
content: [
{
type: "text",
text: responseText,
},
],
isError: true,
};
}
const responseText = JSON.stringify(
{
id: entry.id,
content: entry.content,
category: entry.category,
metadata: entry.metadata,
},
null,
2
);
// Cache the response
setCache(cacheKey, responseText);
return {
content: [
{
type: "text",
text: responseText,
},
],
};
}
if (name === "advanced_search") {
const query = args?.query as string;
const category = args?.category as string | undefined;
const limit = (args?.limit as number) || 5;
if (!query) {
throw new Error("Query parameter is required");
}
let results = searchKnowledge(query, knowledgeBase.length);
// Apply category filter if provided
if (category) {
results = results.filter(
(item) => item.category.toLowerCase() === category.toLowerCase()
);
}
// Apply limit
results = results.slice(0, limit);
const responseText = JSON.stringify(
{
query,
filters: {
category: category || "none",
limit,
},
resultsCount: results.length,
results: results.map((r) => ({
id: r.id,
content: r.content,
category: r.category,
metadata: r.metadata,
})),
},
null,
2
);
// Cache the response
setCache(cacheKey, responseText);
return {
content: [
{
type: "text",
text: responseText,
},
],
};
}
throw new Error(`Unknown tool: ${name}`);
} catch (error) {
const errorMessage = error instanceof Error ? error.message : "Unknown error";
return {
content: [
{
type: "text",
text: JSON.stringify({
error: errorMessage,
}),
},
],
isError: true,
};
}
});
// Error handling
server.onerror = (error) => {
console.error("[MCP Error]", error);
};
process.on("SIGINT", async () => {
await server.close();
process.exit(0);
});
// Start the server
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("Knowledge Base MCP server running on stdio");
}
main().catch((error) => {
console.error("Fatal error:", error);
process.exit(1);
});