2026-08-19 17:40:14 +07:00
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js" ;
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js" ;
import { z } from "zod" ;
2026-08-19 19:00:29 +07:00
import { listDocuments , getDocument , getRelated , semanticSearch , hybridSearch , keywordSearch } from "@mcpedia/core" ;
2026-08-19 17:40:14 +07:00
export function createMcpServer () : McpServer {
const server = new McpServer ({
name : "mcpedia" ,
version : "0.1.0" ,
});
server . registerTool (
"search_documents" ,
{
description :
"Full-text search across the MCPedia knowledge base (Postgres FTS). Returns ranked documents with a headline snippet." ,
inputSchema : z.object ({
query : z.string (). describe ( "Free-text search query" ),
limit : z.number (). int (). positive (). max ( 50 ). optional (),
}),
},
async ({ query , limit }) => {
const hits = await keywordSearch ( query , limit ?? 20 );
return {
content : [{ type : "text" , text : JSON.stringify ( hits , null , 2 ) }],
};
},
);
server . registerTool (
"get_document" ,
{
description :
"Fetch the full markdown body of a document by its slug (e.g. 'docs/websocket/contract')." ,
inputSchema : z.object ({
slug : z.string (). describe ( "Document slug, e.g. 'docs/websocket/contract'" ),
}),
},
async ({ slug }) => {
const doc = await getDocument ( slug );
if ( ! doc ) {
return {
content : [{ type : "text" , text : `Document not found: ${ slug } ` }],
isError : true ,
};
}
return { content : [{ type : "text" , text : doc.body }] };
},
);
server . registerTool (
"list_documents" ,
{
description : "List documents, optionally filtered by section." ,
inputSchema : z.object ({
section : z
. enum ([ "docs" , "writeups" , "research" , "notes" ])
. optional (),
}),
},
async ({ section }) => {
const docs = await listDocuments ({ section });
return {
content : [{ type : "text" , text : JSON.stringify ( docs , null , 2 ) }],
};
},
);
server . registerTool (
"get_related_documents" ,
{
description : "Return documents that share tags with the given slug." ,
inputSchema : z.object ({
slug : z.string (),
limit : z.number (). int (). positive (). max ( 20 ). optional (),
}),
},
async ({ slug , limit }) => {
const related = await getRelated ( slug , limit ?? 5 );
return {
content : [{ type : "text" , text : JSON.stringify ( related , null , 2 ) }],
};
},
);
2026-08-19 19:00:29 +07:00
server . registerTool (
"semantic_search" ,
{
description :
"Semantic (embedding) search across chunked document content. Best for conceptual/paraphrased queries that don't share exact keywords. Returns chunks ranked by cosine similarity." ,
inputSchema : z.object ({
query : z.string (). describe ( "Natural-language query" ),
limit : z.number (). int (). positive (). max ( 50 ). optional (),
}),
},
async ({ query , limit }) => {
const hits = await semanticSearch ( query , limit ?? 10 );
return {
content : [{ type : "text" , text : JSON.stringify ( hits , null , 2 ) }],
};
},
);
server . registerTool (
"hybrid_search" ,
{
description :
"Hybrid search fusing full-text (Postgres FTS) and semantic (embedding) signals via Reciprocal Rank Fusion. Best general-purpose search." ,
inputSchema : z.object ({
query : z.string (). describe ( "Free-text or natural-language query" ),
limit : z.number (). int (). positive (). max ( 50 ). optional (),
}),
},
async ({ query , limit }) => {
const hits = await hybridSearch ( query , limit ?? 10 );
return {
content : [{ type : "text" , text : JSON.stringify ( hits , null , 2 ) }],
};
},
);
2026-08-19 17:40:14 +07:00
return server ;
}
// When run directly (bun run src/index.ts), serve over stdio.
const isMain =
process . argv [ 1 ] && import . meta . url === `file:// ${ process . argv [ 1 ] } ` ;
if ( isMain ) {
const transport = new StdioServerTransport ();
await createMcpServer (). connect ( transport );
}